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_idint
A unique 9-digit integer identifier for each target.
target_namestr
Display name (e.g.
"M 81").alias_namestr
An alias, or
""if unused.target_ra_declist
[RA, Dec]— RA in decimal hours [0, 24), Dec in degrees [-90, 90].lp_filterbool
Trueto use the light-pollution filter for this target.start_minint
Start time as minutes since local midnight (see below).
duration_minint
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 →
126022:30 →
135023:00 →
138000:00 (midnight) →
144001: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.