Configuration file
Starship's config lives in a single TOML file, typically config.toml in the working directory. The Settings page edits it, but you can also edit it directly with any text editor. This page is the reference for the file format, every top-level section, and the recently added knobs.
Location
When you run python scripts/run_starship.py, the loader looks for config.toml in:
--config <path>argument if givenSTARSHIP_CONFIGenvironment variable if set./config.tomlin the current working directory
If none of these exist, the loader also tries starship.toml and ~/.starship/config.toml. If nothing is found, Starship runs with built-in defaults and remembers config.toml in the cwd as the default save target. Save once from the Settings page to materialise the file.
How load/save works
- The loader fills a typed dataclass tree from the parsed TOML. Missing sections and missing keys fall back to their defaults, so a short config that only sets a few values is fine.
- Unknown keys are ignored. Values are coerced to the field's type (a TOML int written where a float is expected is accepted, and vice versa).
save()first copies the existing file toconfig.toml.bak, then writes the new file. The file is written as UTF-8. It is hand-rendered TOML (stdlib only), with a header comment warning that a Starship-initiated rewrite replaces any hand-added comments.- The loader records where the config came from (
source_path). Save writes back to that location, so config edits round-trip cleanly.source_pathis internal bookkeeping, not part of the TOML schema.
Schema
Every section is optional. Top-level sections:
[meta]—config_version(schema version; see below)[solo]— AAG CloudWatcher (host, poll/timeout)[safety]— supervisor dwell/stale windows[observatory]— enclosure type (roll_off|dome), roof/mount interlocks, and the dome geometry (dome_*) that ASDM slit slaving uses[web]— host, port, dashboard auto-open, optional Basic Auth[audit]— DB path, text log, retention, excluded topics[response]— Response Engine plans[ascom]— per-role ProgIDs + a COM watchdog[alpaca]— per-role host/port/device[phd2]— host, port, control permission[allsky]— snapshot URL, poll interval, TLS[cctv]— per-camera RTSP/snapshot URLs[frame_acceptance]— per-frame quality gates that reject bad LIGHT subs as they're captured (default off)[guiding_watchdog]— hold the next sub until PHD2 guiding recovers (default off)[flat]— Auto Flat acquisition (target-ADU exposure search per filter); see the Auto Flat topic[filters]— the filter library (per-filter refocus exposure + focuser offset); see the Filter wheel topic[fits]— images directory, folder layout (flatornight_target, v22.2.5), stretch[planetarium]— backend (cdc), host, port[platesolve]— ASTAP path, radius, timeout[console]— default object name[sequencer]— plans dir, journal limit[cooling]— Peltier cooling[autofocus]— autofocus + Focus Wizard (the learned Refocus recipe lives separately infocus_recipe.json, not in the TOML)[camera]/[focuser]— sensor / focuser characteristics (mostly PENDING in v19)[site]— observatory coordinates[mount]- slew and meridian-flip behaviour (flip_mode, see below)[polar]— plate-solve polar alignment
[alerts]— email + Pushover notifications[pointing]— precise slew-and-center[rotator]— rotator + plate-solve rotation[scheduler]— multi-night dispatcher[blockscript]— L4 session state machines[health]— health monitor thresholds[memory]— memory-pressure responses[diag]— diagnostics ring buffer
Each section is documented in its own help topic where one exists; this page focuses on the file itself and the newest knobs.
Environment variable overrides
Any field in [solo], [safety], [web], [audit], [phd2], or [allsky] can be overridden via STARSHIP_<SECTION>_<FIELD> (uppercased). For example:
STARSHIP_WEB_PORT=8090overrides[web].portSTARSHIP_SOLO_HOST=192.168.1.50overrides[solo].host
The override is type-aware: booleans accept 1/true/yes, ints and floats are parsed, and a value that won't parse is logged and ignored rather than crashing. Overrides apply after the file is loaded, so they win over the file.
Note: override support currently covers only those six sections. Other sections (e.g. [autofocus]) are not env-overridable — edit the file or the Settings page for those.
TOML quirks
- Boolean:
enabled = true(notTrue) - Floats need a decimal:
interval_s = 5.0(not5) - Strings: double quotes only (
"foo", not'foo') - Inline tables:
{ key = value, key = value }with bare keys (not JSON-style"key": value) - Optional fields (e.g.
cooling.default_setpoint_c,autofocus.gain) are written commented-out when unset; uncomment to enable them.
The Sequencer's plan-policy embedding and the Response Engine plans use inline tables — the format is sensitive to TOML rules. Easiest to edit those through their structured editors and let Starship generate the TOML.
Schema version
The config carries a [meta] config_version so future schema changes can migrate old files cleanly. The current build's schema version is 2. A config written by an older Starship (no [meta] section) loads as version 1 — Starship reads it fine and re-saves it at the current version. Every added section has a safe default, so an older file simply gains the new sections at their defaults on the next save; nothing you set is lost. A config newer than the running build fails validation with a clear message rather than being silently misread.
Validation
validate() runs a long list of range/consistency checks and returns a list of human-readable errors; the Settings page surfaces them before saving. A few that bite in practice:
web.auth_enabled = truerequires a non-emptyauth_usernameand apbkdf2_sha256$...hash, or validation fails (no half-open auth).mount.min_altitude_deg > 0requires[site]coordinates to be set.cooling.default_setpoint_cmust be between -50 and +30 °C.- Autofocus HFD ordering is enforced:
near_focus_hfd < hfd_target_near < hfd_target_start, andmax_hfd_allowed >= near_focus_hfd. flat.flat_typemust be one ofpanel,manual,sky_dusk, orsky_dawn,flat.target_adu > 0,flat.max_err_pctin(0, 100], andflat.max_exposure_s >= flat.min_exposure_s.- Frame-acceptance and guiding-watchdog thresholds must be
>= 0(0 = off), withframe_acceptance.central_region_pctin1–100andguiding_watchdog.poll_interval_s > 0.
Dashboard access & optional Basic Auth
By default the dashboard binds to 127.0.0.1:8765 (local machine only). If you set web.host = "0.0.0.0" to reach it from other machines, the dashboard is exposed to your whole LAN.
For an extra layer on the LAN you can enable HTTP Basic Auth (default off):
[web]
host = "0.0.0.0"
auth_enabled = true
auth_username = "your-name"
auth_password_hash = "pbkdf2_sha256$...." # from scripts/hash_password.py
Generate the hash once with python scripts/hash_password.py. The plaintext password is never stored — only the salted PBKDF2-SHA256 hash (240,000 iterations). A malformed or empty stored hash fails closed (access denied) rather than crashing.
Important: Basic Auth here is defense-in-depth on a trusted LAN, not a substitute for network security. Do not expose this port directly to the internet. For remote access, keep the dashboard LAN-only and reach it through the Cloudflare Tunnel + signed JWT (the Nebula Nest model) or a VPN.
Precise pointing — [pointing]
Precise pointing does slew → plate-solve → sync → re-slew until the solved position is within tolerance. Used by the console precise_slew command and by sequencer slew steps with precise=true.
[pointing]
exposure_s = 4.0 # plate-solve frame exposure
binning = 2 # bin 2x2 by default: lighter load, faster solve (was 1)
tolerance_arcmin = 1.0 # done when the solved offset is below this
max_iterations = 3 # cap on solve/slew loops
settle_s = 2.0 # dwell after each slew before exposing
Note: binning now defaults to 2 (it was 1 in earlier builds). The 2x2 read downloads and solves faster with no practical loss for centering.
Meridian flips - [mount]
flip_mode="off"(default) |"halt"|"flip". Off = do nothing at the meridian; halt = end the run cleanly once past it; flip = re-slew to force the pier swap (supervise the first live flip at the mount).flip_min_past_meridian_min,flip_pause_before_min,flip_settle_s,flip_recenter,flip_pier_side_check- the clamps aflip/haltuses.- Precedence: a plan whose meridian mode is
inherit(the editor default) followsflip_mode. A plan set tohaltorflipOVERRIDES it -flip_mode = "off"is not a master switch. The sequencer logs a WARNING, journalsmeridian.overrideonce per run and shows a run-monitor banner when a plan widens this setting. Scheduler chunks use[scheduler].flip_handling(inherit= followflip_mode).
Parking - [observatory]
verify_park_timeout_s=120(default). How long a park is given to confirm. Since v22.2.1 this is also the budget of a sequence'sparkaction (it used a fixed 120 s before); the blockscript and the scheduler always used it. The wait readsAtParkandSlewinglive from the driver and its timeout says whether the driver never started a park slew, the slew was interrupted, or it is still running - raise this value for a slow mount that parks from far away.park_mount_on_roof_close=false(default). When true, a console roof close parks the mount first. When false, closing the roof over an unparked mount is refused until you confirm it; nothing parks.park_before_roof_close=true(default). The ORDER of the make-safe pair (park, then close) - in a blockscript's make-safe tiers and, since v22.2.7, in a sequence'son_end/on_error/on_suspendphases too, however the plan lists the two actions (the roof refuses to close on an unparked mount, so a plan that listed Close roof before Park mount never parked).false= close first, then park, for a roll-off roof that clears the tube in any position (needsroof_motion_requires_safe_mount = false). It does not add a park anywhere.
Coordinate frame - [mount]
precess_targets=true(default, v22.2.0). Every target Starship commands is J2000 (resolver, catalogue, focus stars, plate solves). A mount whose driver reports a topocentricEquatorialSystemexpects the equator of date, about 22 arcmin away in 2026, so slews and syncs are precessed first and the slew job's result records the conversion (frame.converted,frame.delta_arcmin). A mount that reports J2000 gets the numbers unchanged; one that reports another frame, or cannot say, gets raw J2000 and one warning in the log.falserestores the pre-v22.2.0 behaviour (raw J2000 always). Coordinates read back from the mount itself (the meridian flip, the return from a focus star) are never converted.
Camera download - [camera]
imagearray_fast_path=true(default, v22.2.3). Read a COM (ASCOM) camera's
ImageArray straight into numpy through the driver's own COM interface instead of pywin32's tuple-of-ints conversion, which costs about 40 bytes per pixel for the seconds a frame takes to read (about 1.1 GB for a 26 megapixel sensor - enough to trip the health monitor's memory floor on an 8 GB PC). Set it to false only if a driver misbehaves with the new path; Starship then logs once and uses the classic conversion.
Autofocus — [autofocus] (selected new knobs)
The [autofocus] section is large (see the Autofocus help topic for the full list). Two recently added knobs:
[autofocus]
hfd_integration_frames = 3 # frames averaged per measurement point (Voyager-style)
hfd_integration_near_hfd = 0.0 # only integrate when measured HFR <= this; above it use 1 frame; 0 = integrate everywhere
wizard_slew_to_focus_star = false
hfd_integration_near_hfd(default0.0): integration (averaginghfd_integration_framesframes to beat seeing) is only worth the extra exposures near focus, where HFR is small and the relative seeing noise is large. Set this to an HFR value and Starship integrates only when the measured HFR is at or below it (near the bottom of the V); out on the arms it falls back to a single frame to save time.0(default) keeps the old behaviour of integrating at every point. Validated to be>= 0.wizard_slew_to_focus_star(defaultfalse): opt-in. When on, the V-curve wizard slews to a bright catalog focus star (selected by thefocus_star_*magnitude/altitude/distance knobs) before calibrating, then slews back to the original target afterwards. This moves the mount, so it's off by default — leave it off if you don't want the wizard to repoint.
Alerts — [alerts]
Outbound email (SMTP) and Pushover notifications on chosen conditions (v19.62). The AlertsManager subscribes to the event bus and sends when an enabled condition fires. Routing is unit-tested; a real send can only be confirmed against a live SMTP server / Pushover account.
[alerts]
enabled = false
email_enabled = false
smtp_host = ""
smtp_port = 587
smtp_user = ""
smtp_password = ""
smtp_tls = true # STARTTLS
email_from = ""
email_to = "" # comma-separated recipients
pushover_enabled = false
pushover_user_key = ""
pushover_app_token = ""
on_sequence_complete = true
on_sequence_failed = true
on_sequence_stopped = false
on_unsafe = true
on_safe = false
on_safety_error = false # safety monitor error / stale data
Frame acceptance — [frame_acceptance]
Per-frame quality gates that judge each LIGHT sub as it's captured and reject the bad ones (the NINA/Voyager-style frame filter). Default off — with enabled = false every frame is kept, exactly as before. Each gate is judged over a centred crop of the frame and any gate left at 0 is individually disabled, so you can turn on just the one or two you care about.
[frame_acceptance]
enabled = false
max_hfr_px = 0.0 # reject if the median HFR exceeds this (px); 0 = off
min_star_count = 0 # reject if fewer stars are detected; 0 = off
max_eccentricity = 0.0 # reject if median star elongation exceeds this (0 = round, 1 = line); 0 = off
max_background_adu = 0.0 # reject if the background exceeds this (clouds/moon/light leak); 0 = off
reshoot_max = 0 # re-take a rejected sub up to N times (rejects don't count toward the goal); 0 = no reshoot
abort_after_consecutive = 0 # fail the exposure step after N rejects in a row (clouded over); 0 = off
move_rejects = true # move rejected frames to a sibling 'rejected/' folder so the light stack stays clean
central_region_pct = 60.0 # analyse this centred crop (quality is judged near the optical axis) for speed
per_filter_json = "" # per-filter threshold overrides (see below)
A rejected sub is journaled and, with move_rejects on (the default), set aside in a rejected/ folder next to your lights so your stacker never sees it. reshoot_max lets a rejected sub be re-taken so a single bad frame doesn't cost you a slot, and abort_after_consecutive stops the step after a run of rejects so a clouded-over night stops wasting exposures instead of grinding on.
Narrowband subs naturally have far fewer stars and larger HFR than broadband, so one global min_star_count is wrong for them. Set per_filter_json to a JSON object mapping a filter name to a subset of {max_hfr_px, min_star_count, max_eccentricity, max_background_adu}; any gate you don't override falls back to the global value. Example:
per_filter_json = "{\"Ha\": {\"min_star_count\": 4, \"max_hfr_px\": 5.0}}"
Validation keeps every threshold >= 0 (0 = off) and central_region_pct in 1–100.
Guiding watchdog — [guiding_watchdog]
Holds the next sub until PHD2 guiding has recovered, so the rig never starts an exposure while guiding is lost or degraded. It pairs with frame acceptance (which rejects a sub ruined mid-exposure): the watchdog stops a bad one from starting, frame acceptance throws away one that went bad partway through. Default off — no gating. It only acts while PHD2 is actually guiding and control is enabled for the plan.
[guiding_watchdog]
enabled = false
rms_max_px = 0.0 # hold if the total guide RMS exceeds this (px); 0 = wait on star-loss only
recover_timeout_s = 120.0 # give up waiting after this many seconds; the exposure step then fails (on-error policy)
poll_interval_s = 3.0 # how often to re-check PHD2 state while holding
With rms_max_px = 0 the watchdog only waits out an outright lost guide star; set a pixel value to also hold when the RMS climbs above your tolerance. Validation keeps rms_max_px >= 0, recover_timeout_s >= 0, and poll_interval_s > 0.
Auto Flat — [flat]
The [flat] section holds the Auto Flat plan (the per-filter target-ADU exposure search, panel/manual/sky flat source, and end-of-run park/close options). It's edited from Settings → Flat device → Auto Flat and has its own Auto Flat help topic with the full workflow — this page just notes that it lives in the config file. flat_type accepts "panel", "manual", "sky_dusk", or "sky_dawn"; the per-filter plan is the filters array of small inline tables (easiest to edit through the Auto Flat form).
Filter library — [filters]
The [filters] section holds your named filter library — one entry per filter carrying a per-filter refocus exposure (the autofocus exposure used when refocusing on that filter, since dim narrowband needs a longer focus exposure than broadband) and a focuser offset. An empty library falls back to the names your filter wheel reports, else the built-in defaults. The virtual OSC (one-shot colour) filter is offered automatically for a colour camera. It's edited from the filter-library editor and documented in the Filter wheel help topic; each library entry is an inline table {name, refocus_exposure_s, focus_offset}, and a missing or 0 refocus exposure falls back to the global [autofocus].exposure_s.
Memory management
Three sections cooperate on RAM-tight machines (e.g. a LattePanda):
[memory]
enabled = true
gc_on_warning = true # gc.collect() when the memory check is DEGRADED
drop_preview_cache_on_warning = true # also drop rendered FITS previews under pressure
[diag]
ring_buffer_size = 2000 # in-memory recent-log lines kept for the diagnostics bundle
max_log_line_length = 0 # 0 = no truncation
The memory thresholds live in [health], so there is a single source of truth for the limits and [memory] only chooses the responses:
[health]
mem_warn_mb = 2200.0 # RSS DEGRADED (leak watch); above a normal ~1.5GB plateau
mem_critical_mb = 3000.0 # RSS leak-guard ceiling (raised from 1400). Only escalates if system memory is ALSO low.
sys_mem_warn_pct = 15.0 # < this % system RAM available -> DEGRADED
sys_mem_critical_polls = 3 # the floor must hold this many consecutive polls before it is imminent OOM (v22.2.3);
# a dip during a frame download never counts
sys_mem_critical_pct = 8.0 # < this % available -> CRITICAL (real OOM risk)
sys_mem_critical_mb = 512.0 # OR < this many MB available -> CRITICAL (whichever first)
Important: "imminent OOM" means the system is low on RAM, not that this process crossed a fixed RSS line. The free-system-memory check is primary; the RSS thresholds are a secondary leak guard — a high RSS only escalates safety when system memory is also low. This stops a roomy machine reporting a false "imminent OOM". mem_critical_mb defaults to 3000.0 MB (raised from an earlier 1400) so a stable ~1.5 GB working set reads healthy.
Current RSS, system-available memory, and a leak-rate (growth_mb_per_min) are reported in /api/health/status and in memory.json inside the diagnostics zip. You can also reclaim on demand: POST /api/console/free_memory runs a GC and clears the FITS preview cache, returning how much RSS was freed.
Polar alignment — [polar]
The [polar] section configures the plate-solve polar alignment routine (see the Polar alignment help topic for the full procedure). It's used from Settings → Mount → Polar alignment.
[polar]
exposure_s = 4.0 # exposure for each solve frame
binning = 1
turn_deg = 80.0 # how far to ask you to turn the mount in RA
min_turn_deg = 20.0 # below this the axis can't be measured - refuse
positions = 3 # 3 = ask for a third position as a cross-check (you can still continue with two); 2 = never ask
max_spread_arcmin = 0.75 # three positions must agree on the axis to within this, or the run refuses
target_arcmin = 1.0 # accuracy you're aiming for
apply_refraction = true # aim at the refracted pole - leave on
adjust_interval_s = 3.0 # how often the error re-solves while you adjust
max_adjust_min = 30.0 # auto-end the live readout after this long
# Motorized adjustment is PENDING (the loop is built for it but the hardware
# path is not wired or calibrated). These stay inert for now:
motorized_enabled = false
motorized_alt_steps_per_arcmin = 0.0
motorized_az_steps_per_arcmin = 0.0
Requires site coordinates, a connected camera and ASTAP. A mount connection is not required — Starship never slews during polar alignment, so this works on a hand controller with no ASCOM driver at all. The mount should be tracking during the live readout, though: if it isn't, Starship detects the sidereal drift in the solved frames and stops rather than showing you a number that is quietly drifting onto a false pole.