Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions Config/slicecamd.cfg.in
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,13 @@ FINE_ACQUIRE_SETTLE_FRAMES=2
#
FINE_ACQUIRE_SETTLE_SEC=3.0

# FINE_ACQUIRE_MOVE_TIMEOUT=<sec>
# Max time <sec> to wait for acamd to report a goal-shift correction executed
# before the settle begins. On timeout the settle proceeds anyway. Set to 0
# to disable the wait.
#
FINE_ACQUIRE_MOVE_TIMEOUT=60.0

# FINE_ACQUIRE_GAIN=<g>
# Proportional gain <g> = {0..1} applied to the commanded offset when the residual
# is at or below FINE_ACQUIRE_GAIN_THRESHOLD arcsec.
Expand Down
7 changes: 6 additions & 1 deletion acamd/acam_interface.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1443,6 +1443,7 @@ namespace Acam {
const int attempts = this->target.attempts;
const std::string filter = this->motion.get_current_filtername();
const std::string cover = this->motion.get_current_coverpos();
const int64_t ptoffset_seq = this->target.ptoffset_seq.load();

// unless forced, only publish if there was a change in any one of these
//
Expand All @@ -1452,14 +1453,16 @@ namespace Acam {
nacquired == this->last_status.nacquired &&
attempts == this->last_status.attempts &&
filter == this->last_status.filter &&
cover == this->last_status.cover ) return;
cover == this->last_status.cover &&
ptoffset_seq == this->last_status.ptoffset_seq ) return;

this->last_status.acquire_mode = acquire_mode;
this->last_status.is_acquired = is_acquired;
this->last_status.nacquired = nacquired;
this->last_status.attempts = attempts;
this->last_status.filter = filter;
this->last_status.cover = cover;
this->last_status.ptoffset_seq = ptoffset_seq;

// assemble the telemetry into a json message
//
Expand All @@ -1473,6 +1476,7 @@ namespace Acam {
jmessage_out[Key::Acamd::BACKGROUND] = this->astrometry.get_background();
jmessage_out[Key::Acamd::FILTER] = filter;
jmessage_out[Key::Acamd::COVER] = cover;
jmessage_out[Key::Acamd::PTOFFSET_SEQ] = ptoffset_seq;
jmessage_out[Key::PUBTIME] = get_time_us();

try {
Expand Down Expand Up @@ -3692,6 +3696,7 @@ logwrite( function, message.str() );
// send offset to TCS here (returns when offset is complete)
if ( iface->tcsd.pt_offset( ra_off*3600., dec_off*3600., OFFSETRATE )==ERROR) break;
this->allow_large_offset.store(false); // deliberate-offset allowance consumed
this->ptoffset_seq.fetch_add(1, std::memory_order_release);
std::this_thread::sleep_for( std::chrono::seconds(1) );
}

Expand Down
2 changes: 2 additions & 0 deletions acamd/acam_interface.h
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,7 @@ namespace Acam {

std::atomic<bool> is_acquired; ///< set if target acquired successfully
std::atomic<bool> stop_acquisition; ///< set if the acquisition sequence should stop
std::atomic<int64_t> ptoffset_seq{0}; ///< count of pt_offsets executed by do_acquire

double tcs_max_offset;
double tcs_max_putonslit_offset{300.}; ///< max offset (arcsec) for a deliberate goal offset (put-on-slit etc.) applied while guiding; defaults 300 if ACQUIRE_TCS_MAX_PUTONSLIT_OFFSET absent
Expand Down Expand Up @@ -530,6 +531,7 @@ namespace Acam {
int attempts = 0;
std::string filter = "";
std::string cover = "";
int64_t ptoffset_seq = 0;
} last_status;

public:
Expand Down
1 change: 1 addition & 0 deletions common/message_keys.h
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ namespace Key {
inline const std::string ATTEMPTS = "attempts";
inline const std::string SEEING = "seeing";
inline const std::string BACKGROUND = "background";
inline const std::string PTOFFSET_SEQ = "ptoffset_seq"; ///< count of pt_offsets executed by the acquire/guide loop
}

namespace Slicecamd {
Expand Down
66 changes: 58 additions & 8 deletions slicecamd/slicecam_interface.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -562,17 +562,40 @@ namespace Slicecam {
const double cmd_dra = effective_gain * med_dra;
const double cmd_ddec = effective_gain * med_ddec;

if ( this->offset_acam_goal( { cmd_dra, cmd_ddec }, true ) != NO_ERROR ) {
// record the executed-move counter before sending so nothing is missed
const int64_t seq_before = this->acam_ptoffset_seq.load(std::memory_order_acquire);

bool sent_to_acam = false;
if ( this->offset_acam_goal( { cmd_dra, cmd_ddec }, true, &sent_to_acam ) != NO_ERROR ) {
logwrite( function, "ERROR failed to send offset to ACAM" );
this->is_fineacquire_running.store( false, std::memory_order_release );
this->publish_status();
return;
}

// time-based settle: wait for the TCS to physically finish the move before
// sampling resumes. The frame-count settle (settle_frames) is too short in
// wall-clock when autoexpose shortens the exposure (e.g. bright targets), so
// apply a configurable time-based settle on top of it; settle_sec=0 disables.
// On the guiding path the correction only shifts acamd's goal; the
// telescope moves at acamd's next guide solve. Wait for that executed
// move so the settle below is anchored to the move, not the send. A
// stop interrupts the wait; on timeout proceed on the old timer.
if ( sent_to_acam && this->fineacquire_state.move_timeout_sec > 0.0 ) {
std::unique_lock<std::mutex> lock(this->acam_mtx);
const bool moved = this->acam_cv.wait_for( lock,
std::chrono::duration<double>( this->fineacquire_state.move_timeout_sec ),
[this, seq_before]() {
return this->acam_ptoffset_seq.load(std::memory_order_acquire) > seq_before
|| !this->is_fineacquire_running.load(std::memory_order_acquire);
Comment on lines +585 to +586

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 Correlate the sequence with this goal shift

When a regular guide solve is already in progress as the fine-acquire correction is sent, its pt_offset may have been calculated from the old goal but complete after seq_before was sampled. That unrelated completion advances the generic counter and makes this predicate succeed, so slicecamd begins settling while its requested goal shift still awaits the next guide cycle; short slicecam exposures can therefore reproduce the double-offset failure this change is intended to prevent. The acknowledgement needs to identify a move incorporating this specific goal revision, rather than merely any later pt_offset.

Useful? React with 👍 / 👎.

Comment on lines +582 to +586

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 Notify the waiter when fine-acquire stops

If ACAM stalls, stops guiding, or otherwise produces no further status publication, an operator's fineacquire stop only clears is_fineacquire_running and never notifies acam_cv. This wait_for therefore cannot reevaluate its stop predicate until the configured 60-second timeout, leaving the framegrab thread blocked even though the stop command has returned. Notify acam_cv when clearing the running flag so stop actually interrupts the wait.

Useful? React with 👍 / 👎.

});
if ( !this->is_fineacquire_running.load(std::memory_order_acquire) ) return;
if ( !moved ) {
oss.str("");
oss << "WARNING goal shift not executed within "
<< this->fineacquire_state.move_timeout_sec
<< " s; proceeding on the old timer";
logwrite( function, oss.str() );
}
}

// time-based settle after the executed move; settle_sec=0 disables
if ( this->fineacquire_state.settle_sec > 0.0 ) {
std::this_thread::sleep_for( std::chrono::duration<double>( this->fineacquire_state.settle_sec ) );
}
Expand Down Expand Up @@ -924,6 +947,10 @@ namespace Slicecam {
Common::extract_telemetry_value( jmessage, Key::PUBTIME, pubtime );
this->last_acam_pubtime.store( pubtime, std::memory_order_relaxed );

int64_t seq=0;
Common::extract_telemetry_value( jmessage, Key::Acamd::PTOFFSET_SEQ, seq );
this->acam_ptoffset_seq.store( seq, std::memory_order_relaxed );

// wake any thread waiting on ACAM state (e.g. fineacquire)
std::lock_guard<std::mutex> lock(this->acam_mtx);
this->acam_cv.notify_all();
Expand Down Expand Up @@ -1418,6 +1445,19 @@ namespace Slicecam {
applied++;
}
else
if ( config.param[entry] == "FINE_ACQUIRE_MOVE_TIMEOUT" ) {
try { this->fineacquire_state.move_timeout_sec = std::stod( config.arg[entry] ); }
catch ( const std::exception &e ) {
message.str(""); message << "ERROR invalid FINE_ACQUIRE_MOVE_TIMEOUT "
<< config.arg[entry] << ": " << e.what();
logwrite( function, message.str() );
return ERROR;
}
message.str(""); message << "SLICECAMD:config:" << config.param[entry] << "=" << config.arg[entry];
logwrite( function, message.str() );
applied++;
}
else
if ( config.param[entry] == "FINE_ACQUIRE_GAIN" ) {
try { this->fineacquire_state.gain = std::stod( config.arg[entry] ); }
catch ( const std::exception &e ) {
Expand Down Expand Up @@ -2579,11 +2619,16 @@ namespace Slicecam {
* @details When guiding is enabled, the offsets will be applied to the ACAM
* goal so that the ACAM will guide on the offset position. When
* not guiding, the offsets are sent directly to the TCS as PT offsets.
* @param[in] offsets pair { dRA, dDEC }
* @param[in] offsets pair { dRA, dDEC }
* @param[in] fineacquire optional: add the fineguiding token for acamd
* @param[out] sent_to_acam optional: set true when the guiding path was
* taken (goal shift sent to acamd, executed at
* its next guide solve), false when the offset
* went directly (and synchronously) to the TCS
* @return ERROR | NO_ERROR
*
*/
long Interface::offset_acam_goal(const std::pair<double, double> &offsets, std::optional<bool> fineacquire) {
long Interface::offset_acam_goal(const std::pair<double, double> &offsets, std::optional<bool> fineacquire, bool *sent_to_acam) {
const char* function = "Slicecam::Interface::offset_acam_goal";

auto [ra_off, dec_off] = offsets; // local copy
Expand All @@ -2595,6 +2640,11 @@ namespace Slicecam {
// but must allow ACAM to perform the offset.
//
bool is_guiding = this->is_acam_guiding.load();
if (sent_to_acam) *sent_to_acam = is_guiding;

// arcsec copies for the log line (the guiding path sends degrees)
const double ra_off_arcsec = ra_off * 3600.;
const double dec_off_arcsec = dec_off * 3600.;

// send the offsets now
//
Expand Down Expand Up @@ -2632,7 +2682,7 @@ namespace Slicecam {
}

std::ostringstream message;
message << "requested offsets dRA=" << ra_off << " dDEC=" << dec_off << " arcsec";
message << "requested offsets dRA=" << ra_off_arcsec << " dDEC=" << dec_off_arcsec << " arcsec";
logwrite(function, message.str());

return NO_ERROR;
Expand Down
4 changes: 3 additions & 1 deletion slicecamd/slicecam_interface.h
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ namespace Slicecam {
int settle_frames = 0; ///< countdown of frames to discard while telescope settles
int settle_count = 2; ///< configured: frames to discard after each move
double settle_sec = 3.0; ///< time-based settle (sec) after each move; 0 disables (needed when autoexpose shortens frames so the frame-count settle is too brief in wall-clock)
double move_timeout_sec = 60.0; ///< max wait (sec) for acamd to report the goal-shift move executed; 0 disables the wait
int consecutive_centroid_failures = 0; ///< counts consecutive centroid failures
// exposure compensation (shared by the reactive trim and, later, autoexpose)
double exptime_min = 0.1; ///< clamp: minimum auto-adjusted exposure (sec)
Expand Down Expand Up @@ -193,6 +194,7 @@ namespace Slicecam {
std::atomic<bool> is_acam_guiding; ///< is acam guiding?

std::atomic<int64_t> last_acam_pubtime{0}; ///< pubtime (us) of latest received acamd status
std::atomic<int64_t> acam_ptoffset_seq{0}; ///< pt_offset execution counter from latest acamd status

// Latest target (goal) coords published on Topic::TARGETINFO
// NAN until a TARGETINFO arrives, so manual runs with no sequencer target log nan.
Expand Down Expand Up @@ -355,7 +357,7 @@ namespace Slicecam {
long fan_mode( std::string args, std::string &retstring );
long gain( std::string args, std::string &retstring );

long offset_acam_goal(const std::pair<double, double> &offsets, std::optional<bool> fineacquire=std::nullopt);
long offset_acam_goal(const std::pair<double, double> &offsets, std::optional<bool> fineacquire=std::nullopt, bool *sent_to_acam=nullptr);

long collect_header_info( std::unique_ptr<Andor::Interface> &slicecam );

Expand Down
Loading