diff --git a/Config/slicecamd.cfg.in b/Config/slicecamd.cfg.in index 0e977379..b67a96ac 100644 --- a/Config/slicecamd.cfg.in +++ b/Config/slicecamd.cfg.in @@ -84,17 +84,32 @@ FINE_ACQUIRE_BACKGROUND=(80 165 30 210) # FINE_ACQUIRE_MIN_SAMPLES=3 +# FINE_ACQUIRE_PREC= +# MAD scatter threshold per axis (arcsec). A correction is only commanded when +# the per-axis scatter of the collected samples is within this; a noisier batch +# is discarded and re-gathered rather than moving on an untrustworthy median. +# +FINE_ACQUIRE_PREC=0.4 + # FINE_ACQUIRE_SETTLE_FRAMES= # frames discarded after each telescope move to allow settling before # centroid measurements resume. # FINE_ACQUIRE_SETTLE_FRAMES=2 +# FINE_ACQUIRE_SETTLE_SEC= +# Seconds to wait after a commanded telescope move before evaluating the next +# frame. The move plus CCD readout takes time; acting on a frame grabbed mid- +# move/mid-settle gives a wrong correction. Time-based, independent of cadence. +# +FINE_ACQUIRE_SETTLE_SEC=4 + # FINE_ACQUIRE_GAIN= -# Proportional gain = {0..1} applied to the commanded offset when the residual -# is at or below FINE_ACQUIRE_GAIN_THRESHOLD arcsec. +# Proportional gain applied to the commanded offset. Use 1.0: a fine-tune +# step should apply the full measured offset -- deliberately under-correcting a +# known offset is never beneficial here. # -FINE_ACQUIRE_GAIN=0.7 +FINE_ACQUIRE_GAIN=1.0 # FINE_ACQUIRE_GAIN_LARGE= # Proportional gain applied when the residual exceeds FINE_ACQUIRE_GAIN_THRESHOLD. diff --git a/slicecamd/slicecam_interface.cpp b/slicecamd/slicecam_interface.cpp index 01604a4b..b99b8771 100644 --- a/slicecamd/slicecam_interface.cpp +++ b/slicecamd/slicecam_interface.cpp @@ -269,6 +269,13 @@ namespace Slicecam { void Interface::do_fineacquire() { const char* function = "Slicecam::Interface::do_fineacquire"; + // After commanding a TCS move, wait settle_sec before evaluating another + // frame. The move plus CCD readout takes time; acting on an image grabbed + // mid-move or mid-settle yields a wrong correction. Time-based so it does + // not depend on the frame cadence. + // + if ( std::chrono::steady_clock::now() < this->fineacquire_state.settle_until ) return; + // skip frames if we are waiting for the telescope to settle after a move // if (this->fineacquire_state.settle_frames > 0) { @@ -302,6 +309,21 @@ namespace Slicecam { return; } + // Reject a duplicate frame. A commanded framegrab does not guarantee the + // image has refreshed; processing the same frame twice double-counts + // samples and can command a second move from a pre-move image (overshoot). + // Skip the frame if a cheap subsample signature matches the last one. + // + { + uint64_t sig = 1469598103934665603ULL; // FNV-1a offset basis + const size_t step = img_data.size() > 4096 ? img_data.size() / 4096 : 1; + for ( size_t i = 0; i < img_data.size(); i += step ) { + sig = ( sig ^ static_cast( static_cast( img_data[i] ) ) ) * 1099511628211ULL; + } + if ( sig == this->fineacquire_state.last_frame_sig ) return; // identical frame; wait for a fresh one + this->fineacquire_state.last_frame_sig = sig; + } + // find the star centroid near the aim point // Point centroid; @@ -514,6 +536,21 @@ namespace Slicecam { } } + // Never command a move from a noisy solution. We reach here either with the + // scatter within tolerance, or because we hit max_samples. In the latter + // case, if the scatter is still too high the median is not trustworthy: + // discard the batch and re-gather rather than applying a full-gain move to + // an unreliable position (which causes wrong/overshooting moves). This + // matches the reference engine, which refuses to move until precision is met. + // + if ( !scatter_ok ) { + logwrite( function, "fine acquisition: scatter too high to move safely after " + +std::to_string(n)+" samples (scatter=("+std::to_string(sig_dra)+"," + +std::to_string(sig_ddec)+") > "+std::to_string(prec)+" arcsec); re-gathering" ); + this->fineacquire_state.reset(); + return; + } + // select gain: use gain_large when offset is well above the goal threshold // const double effective_gain = ( offset_arcsec > this->fineacquire_state.gain_threshold_arcsec ) @@ -542,9 +579,13 @@ namespace Slicecam { return; } - // reset samples and discard settle_count frames for telescope settling + // reset samples; ignore frames for settle_count frames AND for settle_sec + // seconds so the telescope move + readout completes before we measure again this->fineacquire_state.reset(); this->fineacquire_state.settle_frames = this->fineacquire_state.settle_count; + this->fineacquire_state.settle_until = std::chrono::steady_clock::now() + + std::chrono::duration_cast( + std::chrono::duration( this->fineacquire_state.settle_sec ) ); } /***** Slicecam::Interface::do_fineacquire **********************************/ @@ -1371,6 +1412,32 @@ namespace Slicecam { applied++; } + if ( config.param[entry] == "FINE_ACQUIRE_PREC" ) { + try { this->fineacquire_state.prec_arcsec = std::stod( config.arg[entry] ); } + catch ( const std::exception &e ) { + message.str(""); message << "ERROR invalid FINE_ACQUIRE_PREC " + << 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++; + } + + if ( config.param[entry] == "FINE_ACQUIRE_SETTLE_SEC" ) { + try { this->fineacquire_state.settle_sec = std::stod( config.arg[entry] ); } + catch ( const std::exception &e ) { + message.str(""); message << "ERROR invalid FINE_ACQUIRE_SETTLE_SEC " + << 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++; + } + if ( config.param[entry] == "FINE_ACQUIRE_EXPTIME_MIN" ) { try { this->fineacquire_state.exptime_min = std::stod( config.arg[entry] ); } catch ( const std::exception &e ) { diff --git a/slicecamd/slicecam_interface.h b/slicecamd/slicecam_interface.h index f8d3920f..bd5142d7 100644 --- a/slicecamd/slicecam_interface.h +++ b/slicecamd/slicecam_interface.h @@ -87,13 +87,16 @@ namespace Slicecam { std::vector ddec_samp; ///< dDEC samples, degrees int max_samples = 10; ///< samples before evaluating a move int min_samples = 3; ///< minimum samples before scatter-gated early exit - double prec_arcsec = 0.1; ///< MAD scatter threshold per axis for early exit (arcsec) + double prec_arcsec = 0.4; ///< MAD scatter threshold per axis; never command a move unless scatter is within this (arcsec) double goal_arcsec = 0.3; ///< convergence threshold, arcsec - double gain = 0.7; ///< gain applied when offset <= gain_threshold_arcsec + double gain = 1.0; ///< gain applied when offset <= gain_threshold_arcsec (full correction; never under-correct a known offset) double gain_large = 1.0; ///< gain applied when offset > gain_threshold_arcsec double gain_threshold_arcsec = 2.0; ///< offset above which gain_large is used 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 = 4.0; ///< configured: seconds to wait after a TCS move before evaluating the next frame + std::chrono::steady_clock::time_point settle_until{}; ///< frames are ignored until this time (post-move settle) + uint64_t last_frame_sig = 0; ///< signature of the last processed frame, to reject duplicate (unrefreshed) frames 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) diff --git a/slicecamd/slicecam_math.cpp b/slicecamd/slicecam_math.cpp index 9ee11908..92eba2c5 100644 --- a/slicecamd/slicecam_math.cpp +++ b/slicecamd/slicecam_math.cpp @@ -185,7 +185,7 @@ namespace Slicecam { * Among all qualifying candidates, select the brightest. * * Step 4: refine to sub-pixel centroid via iterative Gaussian- - * windowed first-moment (centroid_sigma_pix = 2.0, 12 iterations, + * windowed first-moment (centroid_sigma_pix = 1.2, 12 iterations, * eps = 0.01 px). * * All pixel coordinates are FITS 1-based on input and output. @@ -324,12 +324,12 @@ namespace Slicecam { // --- Step 4: iterative Gaussian-windowed first-moment centroid --- // // centroid_halfwin = 4 (CF's --centroid-hw default) - // centroid_sigma_pix = 2.0 (CF's default, NOTE: different from filt_sigma) + // centroid_sigma_pix = 1.2 (matches auto-acq-clean --centroid-sigma 1.2) // centroid_maxiter = 12 (CF's default) // centroid_eps_pix = 0.01 // const int hw = 4; - const double s2 = 2.0 * 2.0; // centroid_sigma_pix^2 + const double s2 = 1.2 * 1.2; // centroid_sigma_pix^2 double cx = static_cast( best_x ); double cy = static_cast( best_y );