Skip to content

tcsd + sequencerd: let the TCS itself satisfy the "ontarget" wait (opt-in) - #483

Open
cfremling wants to merge 1 commit into
mainfrom
feat/auto-ontarget
Open

tcsd + sequencerd: let the TCS itself satisfy the "ontarget" wait (opt-in)#483
cfremling wants to merge 1 commit into
mainfrom
feat/auto-ontarget

Conversation

@cfremling

@cfremling cfremling commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

Reference implementation from on-sky measurement — offered as the working example for the preferred sequencerd implementation; adapt freely.

What the operator's press costs, measured

Every science slew ends with sequencerd blocking on the TCS operator's
manual "ontarget" press (move_to_target, SEQ_WAIT_TCSOP). A shadow rig ran
?ONTARGET at 1 Hz through a whole night (UT 2026-08-25, 64 slews, 59 with
a clean 0→1 transition) alongside the operator's presses:

  • the operator's press trailed the TCS's own on-target flag by
    median +9.7 s, p90 +18 s, max +31.5 s — and never once preceded it
    (0 of 59);
  • at the flip, the TCS's reported position agreed with the commanded
    coordinates to median 0.4″;
  • ~10 minutes of pure wait per night at current cadence.

To get this flag through tcsd

?ONTARGET's reply IS the flag (0/1), but TCS::Interface::send_command
translates every reply that is not on its information-command whitelist
through parse_reply_code: 0 collides with TCS_SUCCESS and renders
"success"; 1 is no valid status code and renders "tcs_undefined" ERROR.

The code here properly exposes the flag status.

The change (120 lines, all opt-in)

  1. tcsd — add ?ONTARGET to send_command's verbatim whitelist
    (tcs_interface.cpp, 1 line + comment). tcs native '?ONTARGET' now
    returns 0/1.

  2. sequencerd — config TCS_AUTO_ONTARGET (default no: deployed
    behaviour is byte-identical until someone flips it). When enabled,
    move_to_target spawns dothread_auto_ontarget for exactly the lifetime
    of the TCSOP wait (an auto_ontarget_active atomic brackets it). The
    watcher acknowledges through the same condition variable the operator's
    press uses — whichever arrives first wins — when two consecutive 1 Hz
    reads
    satisfy all three of:

    ?ONTARGET == 1                          (the TCS settled flag)
    motion    == "tracking"                 (cached 1 Hz tcsd telemetry)
    |telescope − commanded| < TCS_AUTO_ONTARGET_SEP   (default 5″, same cache)
    

    A wrong acknowledgment is deliberately treated as self-limiting: ACAM
    cannot solve a field that is not there, and the acquisition retry
    machinery handles the failure — so these conditions exist to make wrong
    acks rare, not impossible, and no operator press is expected in this
    mode. The comparison is against the SCOPE-frame coordinates
    move_to_target actually commanded (the database coordinates differ by
    the pointmode focal-plane offset), with the RA delta wrapped to the
    shortest arc. The acknowledgment deliberately does not go through
    ontarget(), which clears cancel_flag: a concurrent cancel always
    wins against the automatic path (re-checked at the last instant, and
    never cleared by it).

    All three are required because each alone is measured to fail:

    • the flag is position-agnostic — it reads 1 while settled on the
      previous field (observed at 25–57° separation);
    • separation alone cannot see a hop smaller than its threshold that is
      still in progress (the offset-star expose race);
    • motion alone says nothing about settling.

    On nearby offset-star acquisitions (~2.7′) all three signals transition cleanly
    (flag drops to 0 for ~10 s, motion reads "offsetting"); for nudges below
    a few arcsec nothing transitions and the watcher simply never fires —
    the operator (or the existing settle logic) covers that case unchanged.
    The debounce covers two observed settle bounces (1→0→1) from the logged night.

  3. Config template sequencerd.cfg.in: the two keys, default off.

What does NOT change

  • TCS_AUTO_ONTARGET=no (the default): nothing. The watcher is never
    spawned; the whitelist line only affects the reply text of a query
    nothing currently issues.
  • The operator's press: unchanged in every mode; it races the watcher into
    the same ontarget() and either satisfies the wait.
  • cancel/abort: the watcher exits on cancel_flag, on is_ontarget, and
    when the wait closes (auto_ontarget_active false) — it cannot outlive
    the wait or fire into a later phase.

Validation plan before enabling

The shadow rig keeps running nightly; each night appends ~60 more
slews of (flag, motion, separation, operator) tuples. Enable on-sky only
after N nights show zero would-have-fired-early cases against the operator
baseline. First enable with the operator still watching: the broadcast
notice ("TCS reports on-target … sending ontarget") makes every automatic
ack visible in the sequencer log and GUI.

🤖 Generated with Claude Code

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f56675fd0e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread sequencerd/sequence.cpp
}
if ( !tracking || !( sep_as < this->tcs_auto_ontarget_sep ) ) { consecutive=0; continue; }

if ( ++consecutive < 2 ) continue; // debounce: two reads in a row

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Arm the watcher only after observing a non-settled state

When the new target is within tcs_auto_ontarget_sep of the previous field and the TCS does not drop ?ONTARGET or leave tracking, the previous settled state already satisfies every predicate here. The watcher therefore reaches two consecutive samples and calls ontarget() even though it has never observed the commanded move begin or settle—the exact small-nudge case that is supposed to remain operator-controlled. Require a post-command disqualifying transition before incrementing this counter, or otherwise exclude slews below the threshold.

Useful? React with 👍 / 👎.

Comment thread sequencerd/sequence.cpp Outdated
Comment on lines +2217 to +2218
this->broadcast.notice( function, message.str() );
this->ontarget();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Recheck cancellation before sending the automatic acknowledgment

If an abort sets cancel_flag after the loop condition was checked—for example while the second ?ONTARGET request is in flight—this path still calls ontarget(), whose implementation clears cancel_flag. The waiting sequence can then observe is_ontarget rather than the abort and continue into acquisition or exposure. Recheck auto_ontarget_active and cancel_flag immediately before acknowledging, and ensure the automatic path cannot clear a concurrently issued cancellation.

Useful? React with 👍 / 👎.

Comment thread sequencerd/sequence.cpp Outdated
Comment on lines +2177 to +2178
double tgt_ra_h = radec_to_decimal( this->target.ra_hms ); // decimal hours
double tgt_dec_d = radec_to_decimal( this->target.dec_dms ); // decimal degrees

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Compare telemetry with the coordinates actually sent to the TCS

For targets using a focal-plane point mode such as SLIT or ACAM, these database coordinates are transformed by compute_offset(pointmode, "SCOPE", ...), and move_to_target() sends the resulting ra_out/dec_out to the TCS. REQPOS telemetry describes that scope position, so comparing it with the untransformed input can leave the measured separation larger than the default 5 arcsec and prevent auto-ontarget from ever succeeding. Capture and compare against the commanded scope coordinates instead.

Useful? React with 👍 / 👎.

Comment thread sequencerd/sequence.cpp Outdated
Comment on lines +2205 to +2206
double dra_as = ( this->tcsinfo.ra_h - tgt_ra_h ) * 15.0
* cos( tgt_dec_d * M_PI / 180.0 ) * 3600.0;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Normalize the right-ascension delta across 24 hours

For a target near 00:00 whose measured position falls just across the 24-hour boundary, subtracting the hour values directly produces a delta close to 24 hours rather than a sub-arcsecond delta. The separation test consequently never passes even when the telescope is on target, leaving these observations dependent on the manual acknowledgment. Wrap the RA difference into the shortest signed interval before converting it to arcseconds.

Useful? React with 👍 / 👎.

@cfremling
cfremling force-pushed the feat/auto-ontarget branch 2 times, most recently from 9a91093 to 0451b1f Compare August 26, 2026 06:58
@cfremling

Copy link
Copy Markdown
Collaborator Author

All four review findings addressed in the amended commit:

  1. Arm only after a non-settled observation (P1) — the watcher now requires at least one read that observed a non-settled state (saw_unsettled) before any acknowledgment; read failures and stale telemetry observe nothing. A move too small to disturb any predicate stays with the operator, which was always the documented intent — now it is also the code.
  2. Cancel race (P1) — the acknowledgment no longer goes through ontarget() (which clears cancel_flag); it sets is_ontarget and notifies the same condition variable without touching the cancel, and re-checks cancel_flag/auto_ontarget_active at the last instant. A concurrent cancel always wins: the waiter sees both flags and honours the cancel first.
  3. Commanded vs database coordinates (P2) — the watcher now receives the SCOPE-frame ra_out/dec_out that move_to_target actually sent (post compute_offset(pointmode, "SCOPE", ...)), which is what REQPOS telemetry reports.
  4. RA wrap (P2) — the RA delta is wrapped to the shortest signed arc before conversion to arcseconds.

🤖 Generated with Claude Code

?ONTARGET was invisible through tcsd because send_command translates
every non-whitelisted reply as a status code: the flag IS the reply, so
0 collided with TCS_SUCCESS ('success') and 1 fell through to
'tcs_undefined' ERROR. Whitelist it for verbatim passthrough.

sequencerd: config TCS_AUTO_ONTARGET (default no) spawns a watcher for
exactly the lifetime of move_to_target's TCSOP wait. It calls
ontarget() -- the operator's own path -- when, on two consecutive 1 Hz
reads: ?ONTARGET==1 AND motion==tracking AND |telescope-target| <
TCS_AUTO_ONTARGET_SEP arcsec. All three are required: shadow data
(UT 2026-08-25, 64 slews) shows the flag is position-agnostic, the
separation cannot see sub-threshold hops, and motion says nothing
about settle. Operator ack trailed the flag by median +9.7 s / p90
+18 s and never once preceded it. The operator's press is unchanged;
whichever arrives first wins.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant