Observation Plans

The plan module lets you send multi-target observation plans to the Seestar. The Seestar will autonomously slew to each target at the scheduled time, plate-solve, and begin stacking — just like the official app’s “Plan” feature.

Quick example

from seestarpy import plan

my_plan = {
    "plan_name": "Evening Session",
    "update_time_seestar": "2026.02.28",
    "list": [
        {
            "target_id": 100000001,
            "target_name": "M42",
            "alias_name": "Orion Nebula",
            "target_ra_dec": [5.588, -5.39],
            "lp_filter": True,
            "start_min": 1350,       # 22:30
            "duration_min": 30,
        },
        {
            "target_id": 100000002,
            "target_name": "M31",
            "alias_name": "Andromeda Galaxy",
            "target_ra_dec": [0.712, 41.27],
            "lp_filter": False,
            "start_min": 1380,       # 23:00
            "duration_min": 45,
        },
    ],
}

plan.set_view_plan(my_plan)

That’s it — the Seestar will wait until 22:30 local time, slew to M42, and start imaging. At 23:00 it will move to M31 automatically.

Building a plan dictionary

A plan dictionary has three required keys:

plan_name (str)

A human-readable name for the plan.

update_time_seestar (str)

The date string in "yyyy.MM.dd" format (e.g. "2026.02.28").

list (list of dicts)

One dictionary per target, with the following fields:

Field

Type

Description

target_id

int

A unique 9-digit integer identifier for each target.

target_name

str

Display name (e.g. "M 81").

alias_name

str

An alias, or "" if unused.

target_ra_dec

list

[RA, Dec] — RA in decimal hours [0, 24), Dec in degrees [-90, 90].

lp_filter

bool

True to use the light-pollution filter for this target.

start_min

int

Start time as minutes since local midnight (see below).

duration_min

int

Observation duration in minutes.

Understanding start_min

start_min is the number of minutes after local midnight (00:00) on the Seestar’s internal clock. It is not relative to when the plan starts.

Some examples:

  • 21:00 → 1260

  • 22:30 → 1350

  • 23:00 → 1380

  • 00:00 (midnight) → 1440

  • 01:30 AM → 1530

A helper to convert a (hours, minutes) tuple:

def to_start_min(hour, minute=0):
    """Convert a 24h time to start_min for a plan target."""
    return hour * 60 + minute

to_start_min(22, 30)   # 1350
to_start_min(1, 30)    # 90  — but for post-midnight, add 1440:
1440 + to_start_min(1, 30)  # 1530

Note

For targets scheduled after midnight, use values above 1440. The Seestar’s clock is synced to the phone/host local time via raw.pi_set_time().

Monitoring a running plan

Use get_running_plan() to check the status of the currently executing plan:

from seestarpy import plan

vp = plan.get_running_plan()

if vp is None:
    print("No plan is running")
else:
    print(f"State: {vp['state']}")         # "working", "cancel", etc.
    print(f"Plan:  {vp['plan']['plan_name']}")

    # Per-target status (added by firmware)
    for target in vp["plan"]["list"]:
        name = target["target_name"]
        state = target.get("state", "pending")
        skip = target.get("skip", False)
        print(f"  {name}: {state}  (skip={skip})")

The firmware adds state, lapse_ms, and skip fields to each target as the plan progresses.

Stopping a plan

plan.stop_view_plan()

This sends the same stop_func command that the official app uses. After stopping, get_running_plan() will show the plan with state: "cancel".

Full workflow example

A complete script that sets up the Seestar, submits a plan, monitors progress, and then parks:

import time
from seestarpy import raw, plan

# Sync time and open the arm
raw.pi_set_time()
raw.scope_move_to_horizon()

# Define a two-target plan
tonight = {
    "plan_name": "Friday Night",
    "update_time_seestar": "2026.02.28",
    "list": [
        {
            "target_id": 200000001,
            "target_name": "M 81",
            "alias_name": "Bode's Galaxy",
            "target_ra_dec": [9.926, 69.065],
            "lp_filter": False,
            "start_min": 1260,       # 21:00
            "duration_min": 60,
        },
        {
            "target_id": 200000002,
            "target_name": "M 51",
            "alias_name": "Whirlpool Galaxy",
            "target_ra_dec": [13.498, 47.195],
            "lp_filter": False,
            "start_min": 1320,       # 22:00
            "duration_min": 90,
        },
    ],
}

# Send and start the plan
plan.set_view_plan(tonight)

# Poll until finished or cancelled
while True:
    vp = plan.get_running_plan()
    if vp is None:
        break
    if vp["state"] != "working":
        print(f"Plan ended with state: {vp['state']}")
        break

    # Show progress
    for t in vp["plan"]["list"]:
        print(f"  {t['target_name']}: {t.get('state', 'pending')}")

    time.sleep(60)

# Park when done
raw.iscope_stop_view()
raw.scope_park(True)

Mosaic plans

Extended objects like the North America Nebula (NGC 7000) are larger than the Seestar’s ~0.75 x 1.33 degree field of view. create_mosaic_plan() generates a multi-panel observation plan that tiles a rectangular sky region:

from seestarpy import plan

mosaic = plan.create_mosaic_plan(
    plan_name="NGC 7000 Mosaic",
    center_ra=20.99,       # ~20h 59m
    center_dec=44.53,
    width=3.0,             # 3° wide
    height=2.0,            # 2° tall
    delta_ra=1.0,          # 1° panel spacing in RA
    delta_dec=1.0,         # 1° panel spacing in Dec
    t_total=180,           # 3 hours total
    start_min=1320,        # 22:00
    lp_filter=True,
)

print(f"{len(mosaic['list'])} panels, "
      f"{mosaic['list'][0]['duration_min']} min each")
# 6 panels, 30 min each

# Inspect the grid before sending
for p in mosaic["list"]:
    ra, dec = p["target_ra_dec"]
    print(f"  {p['target_name']}: RA={ra:.3f}h  Dec={dec:.2f}°")

# Send to the Seestar
plan.set_view_plan(mosaic)

Panels are traversed in boustrophedon (snake) order — even rows go left-to-right in RA, odd rows go right-to-left — to minimise slew time between consecutive panels. RA spacing is automatically corrected for cos(dec) so panels cover equal angular extents on the sky.

Polygon plans

create_polygon_plan() generalises the mosaic concept to arbitrary sky regions. Instead of an axis-aligned rectangle, you define the boundary with 3 or more RA/Dec corner points (ordered clockwise or counter-clockwise, non-self-intersecting). The function uses a gnomonic tangent-plane projection internally, so cos(dec) correction and RA wraparound near 0 h/24 h are handled automatically.

Quadrilateral — a tilted rectangle that doesn’t align with the equatorial grid:

from seestarpy import plan

quad = plan.create_polygon_plan(
    plan_name="Veil Nebula",
    corners=[
        (20.75, 30.0),   # bottom-left
        (20.95, 30.0),   # bottom-right
        (20.95, 32.0),   # top-right
        (20.75, 32.0),   # top-left
    ],
    delta_ra=0.5,        # grid spacing in degrees
    delta_dec=0.5,
    t_total=120,         # 2 hours total
    start_min=1320,      # 22:00
)

plan.set_view_plan(quad)

Triangle — useful for irregularly shaped nebulae:

tri = plan.create_polygon_plan(
    plan_name="Triangular Region",
    corners=[
        (12.0, 0.0),
        (12.2, 0.0),
        (12.1, 1.5),
    ],
    delta_ra=0.4,
    delta_dec=0.4,
    t_total=90,
    start_min=1380,      # 23:00
)

Pentagon or more — any simple polygon works:

penta = plan.create_polygon_plan(
    plan_name="Five-sided region",
    corners=[
        (12.0, 0.0),
        (12.1, -0.5),
        (12.2, 0.0),
        (12.15, 0.5),
        (12.05, 0.5),
    ],
    delta_ra=0.3,
    delta_dec=0.3,
    t_total=90,
    start_min=1320,
)

Only grid points that fall inside the polygon boundary are included in the plan. If the grid spacing is larger than the entire region, a single pointing at the centroid is generated.

The panels use the same boustrophedon traversal and cos(dec)-aware spacing as create_mosaic_plan().

Note

create_quadrilateral_plan is available as an alias for create_polygon_plan.

Named plans

create_named_plan() is the quickest way to build a plan — just list your targets by name and start time:

from seestarpy import plan

p = plan.create_named_plan(
    plan_name="Friday Night",
    targets=[
        ("M42",     "21:00"),
        ("NGC 884", "22:30"),
        ("M31",     "23:45"),
    ],
    end_time="01:00",
)

plan.set_view_plan(p)

Each target observes from its start time until the next target begins. The last target (M31) observes until end_time (01:00). In this example, M42 gets 90 min, NGC 884 gets 75 min, and M31 gets 75 min.

Target coordinates are resolved automatically using the CDS Sesame name resolver, which queries SIMBAD, NED, and VizieR. Standard designations all work: Messier (M42), NGC/IC (NGC 884), HD (HD 126675), HIP, Bayer names, etc. An internet connection is required for name resolution.

Mixing names and coordinates

The target specifier can be either a name string or an explicit (ra_hours, dec_deg) pair. You can freely mix both in the same plan:

p = plan.create_named_plan(
    plan_name="Mixed Session",
    targets=[
        ("M42", "21:00"),                   # resolved via Sesame
        ((0.712, 41.27), "22:30"),           # explicit RA/Dec
        ("NGC 7000", "23:30", True),         # with LP filter
    ],
    end_time="01:00",
)

Explicit coordinates skip the network call entirely, which is useful for custom pointings or when working offline.

Per-target light-pollution filter

Each target tuple can optionally include a third element to control the LP filter individually. Targets without the third element use the function-level lp_filter default (False):

p = plan.create_named_plan(
    plan_name="LP Filter Mix",
    targets=[
        ("M42", "21:00", True),     # LP filter ON
        ("M31", "22:30", False),    # LP filter OFF
        ("M51", "23:30"),           # uses default (False)
    ],
    end_time="01:00",
    lp_filter=False,               # default for targets without override
)

Midnight wraparound

Times are expected in 24-hour "hh:mm" format. If a start time is earlier than the previous target’s, it is automatically treated as post-midnight (next calendar day):

p = plan.create_named_plan(
    plan_name="Late Night",
    targets=[
        ("M42", "23:00"),       # 23:00 tonight
        ("M31", "01:30"),       # 01:30 tomorrow — auto-detected
    ],
    end_time="03:00",           # 03:00 tomorrow
)

# M42: start_min=1380, duration=150 min
# M31: start_min=1530, duration=90 min

Resolving a single target

You can also use the name resolver standalone with resolve_name():

from seestarpy.plan import resolve_name

ra, dec = resolve_name("M42")
print(f"RA = {ra:.4f} h, Dec = {dec:.2f}°")
# RA = 5.5881 h, Dec = -5.39°

This returns (ra_hours, dec_deg) — the same convention used throughout seestarpy.

Visualising plans

All plan types (mosaic, polygon, named) produce the same plan dictionary format, so plot_mosaic_plan() works with any of them:

plan.plot_mosaic_plan(quad)    # polygon plan
plan.plot_mosaic_plan(p)      # named plan

This shows panel footprints on a Mollweide projection with RA/Dec grid lines. Requires matplotlib and numpy.