#include "internal.hpp" #include #include namespace target_exposure { std::vector detail::validateController(const ControllerConfig& k) { std::vector errors; const auto require = [&](bool ok, const char* name) { if (!ok) errors.emplace_back(name); }; const auto range = [](double v, double lo, double hi) { return std::isfinite(v) && v >= lo && v <= hi; }; require(range(k.target_gray, 0, 255) && k.target_gray > 0, "target_gray must be in (0, 255]"); require(range(k.deadband_ratio, 0, 1) && range(k.hysteresis_ratio, 0, 1) && std::isfinite(k.large_error_ratio) && k.large_error_ratio > k.deadband_ratio + k.hysteresis_ratio, "large_error_ratio must exceed deadband plus hysteresis"); require(range(k.proportional_gain, 0, 1), "proportional_gain must be in [0, 1]"); require(std::isfinite(k.kp) && k.kp >= 0 && std::isfinite(k.ki) && k.ki >= 0, "PI gains must be nonnegative and finite"); require(std::isfinite(k.integral_abs_limit) && k.integral_abs_limit >= 0, "integral_abs_limit must be nonnegative and finite"); require(range(k.max_step_ratio, 0, 1) && k.max_step_ratio > 0 && k.max_step_ratio < 1, "max_step_ratio must be in (0, 1)"); require(k.same_direction_frames > 0, "same_direction_frames must be positive"); return errors; } bool detail::sameController(const ControllerConfig& a, const ControllerConfig& b) { const auto fields = [](const ControllerConfig& c) { return std::tie(c.target_gray, c.deadband_ratio, c.hysteresis_ratio, c.large_error_ratio, c.proportional_gain, c.kp, c.ki, c.integral_abs_limit, c.max_step_ratio, c.same_direction_frames); }; return fields(a) == fields(b); } std::vector detail::validateMetering(const MeteringConfig& m) { std::vector errors; const auto require = [&](bool ok, const char* name) { if (!ok) errors.emplace_back(name); }; require(std::isfinite(m.trim_fraction_each_side) && m.trim_fraction_each_side >= 0 && m.trim_fraction_each_side < 0.5, "trim_fraction_each_side must be in [0, 0.5)"); require(m.min_target_pixels > 0 && m.min_retained_pixels > 0, "minimum pixel counts must be positive"); require(std::isfinite(m.global_center_fraction) && m.global_center_fraction > 0 && m.global_center_fraction <= 1, "global_center_fraction must be in (0, 1]"); require(m.near_black_level >= 0 && m.saturation_level <= 255 && m.near_black_level < m.saturation_level, "invalid black/saturation thresholds"); return errors; } std::vector validate(const Config& c) { std::vector errors; const auto require = [&](bool ok, const char* name) { if (!ok) errors.emplace_back(name); }; const auto range = [](double v, double lo, double hi) { return std::isfinite(v) && v >= lo && v <= hi; }; require(!c.profile_id.empty(), "profile_id must not be empty"); require(std::isfinite(c.min_exposure_us) && c.min_exposure_us > 0, "min_exposure_us must be positive and finite"); require(std::isfinite(c.max_exposure_us) && c.max_exposure_us >= c.min_exposure_us, "max_exposure_us must be finite and >= min_exposure_us"); require(range(c.empirical_exposure_us, c.min_exposure_us, c.max_exposure_us), "empirical_exposure_us must be within the exposure limits"); require(c.history_window_us > 0 && c.idle_timeout_us > 0, "time windows must be positive"); require(c.detect_confirm_frames > 0 && c.lost_confirm_frames > 0 && c.freeze_frames > 0, "state confirmation/freeze counts must be positive"); require(c.prediction.points == 3 || c.prediction.points == 5, "prediction.points must be 3 or 5"); require(c.history_capacity >= c.prediction.points, "history_capacity must retain the prediction points"); const auto meteringErrors = detail::validateMetering(c.metering); errors.insert(errors.end(), meteringErrors.begin(), meteringErrors.end()); const auto& p = c.prediction; require(range(p.black_level, 0, 255) && p.black_level < 255, "black_level must be in [0, 255)"); require(p.nominal_interval_us >= 0 && p.max_horizon_us > 0, "invalid prediction time limits"); require(std::isfinite(p.max_relative_fit_rmse) && p.max_relative_fit_rmse >= 0, "max_relative_fit_rmse must be nonnegative and finite"); require(range(p.max_near_black_fraction, 0, 1) && range(p.max_saturated_fraction, 0, 1), "prediction clipping fractions must be in [0, 1]"); require(range(p.min_predicted_gray, 0, 255) && range(p.max_predicted_gray, 0, 255) && p.min_predicted_gray < p.max_predicted_gray && p.max_predicted_gray > p.black_level, "invalid predicted gray range"); const auto& k = c.controller; const auto controllerErrors = detail::validateController(k); errors.insert(errors.end(), controllerErrors.begin(), controllerErrors.end()); const auto& r = c.recovery; require(!r.global_reference_gray || (range(*r.global_reference_gray, 0, 255) && *r.global_reference_gray > 0), "global_reference_gray must be in (0, 255]"); require(range(r.deadband_ratio, 0, 1) && range(r.gain, 0, 1), "invalid recovery deadband/gain"); require(range(r.max_step_ratio, 0, k.max_step_ratio) && r.max_step_ratio > 0, "recovery step must be positive and no greater than normal step"); return errors; } bool operator==(const Config& a, const Config& b) { const auto base = [](const Config& c) { return std::tie(c.profile_id, c.revision, c.min_exposure_us, c.max_exposure_us, c.empirical_exposure_us, c.history_window_us, c.idle_timeout_us, c.history_capacity, c.detect_confirm_frames, c.lost_confirm_frames, c.freeze_frames); }; const auto meter = [](const MeteringConfig& c) { return std::tie(c.trim_fraction_each_side, c.min_target_pixels, c.min_retained_pixels, c.global_center_fraction, c.near_black_level, c.saturation_level); }; const auto pred = [](const PredictionConfig& c) { return std::tie(c.enabled, c.photometric_model_confirmed, c.points, c.black_level, c.nominal_interval_us, c.max_horizon_us, c.max_relative_fit_rmse, c.max_near_black_fraction, c.max_saturated_fraction, c.min_predicted_gray, c.max_predicted_gray); }; const auto rec = [](const RecoveryConfig& c) { return std::tie(c.global_reference_gray, c.deadband_ratio, c.gain, c.max_step_ratio); }; return base(a) == base(b) && meter(a.metering) == meter(b.metering) && pred(a.prediction) == pred(b.prediction) && detail::sameController(a.controller, b.controller) && rec(a.recovery) == rec(b.recovery); } std::vector validate(const ContinuousConfig& c) { std::vector errors; if (!std::isfinite(c.min_exposure_us) || c.min_exposure_us <= 0) errors.emplace_back("min_exposure_us must be positive and finite"); if (!std::isfinite(c.max_exposure_us) || c.max_exposure_us < c.min_exposure_us) errors.emplace_back("max_exposure_us must be finite and >= min_exposure_us"); if (c.idle_timeout_us <= 0) errors.emplace_back("idle_timeout_us must be positive"); if (c.nominal_interval_us <= 0) errors.emplace_back("nominal_interval_us must be positive"); if (c.smoothing_tau_us < 0) errors.emplace_back("smoothing_tau_us must be nonnegative"); if (c.confirmation_us < 0) errors.emplace_back("confirmation_us must be nonnegative"); if (!std::isfinite(c.max_step_ratio_per_second) || c.max_step_ratio_per_second <= 0) errors.emplace_back("max_step_ratio_per_second must be positive and finite"); for (const auto& error : detail::validateController(c.region_controller)) errors.emplace_back("region_controller." + error); for (const auto& error : detail::validateController(c.global_controller)) errors.emplace_back("global_controller." + error); return errors; } bool operator==(const ContinuousConfig& a, const ContinuousConfig& b) { return a.min_exposure_us == b.min_exposure_us && a.max_exposure_us == b.max_exposure_us && a.idle_timeout_us == b.idle_timeout_us && a.preserve_confirmation_on_idle == b.preserve_confirmation_on_idle && a.time_aware == b.time_aware && a.nominal_interval_us == b.nominal_interval_us && a.smoothing_tau_us == b.smoothing_tau_us && a.confirmation_us == b.confirmation_us && a.max_step_ratio_per_second == b.max_step_ratio_per_second && detail::sameController(a.region_controller, b.region_controller) && detail::sameController(a.global_controller, b.global_controller); } #define TEC_ENUM_CASE(type, name) case type::name: return #name const char* toString(FeedbackSource v) noexcept { switch (v) { TEC_ENUM_CASE(FeedbackSource, Global); TEC_ENUM_CASE(FeedbackSource, ManualRoi); TEC_ENUM_CASE(FeedbackSource, YoloRegions); } return "Unknown"; } const char* toString(FeedbackFallbackReason v) noexcept { switch (v) { TEC_ENUM_CASE(FeedbackFallbackReason, None); TEC_ENUM_CASE(FeedbackFallbackReason, RegionUnavailable); TEC_ENUM_CASE(FeedbackFallbackReason, InvalidRegionMeasurement); } return "Unknown"; } const char* toString(FeedbackResetReason v) noexcept { switch (v) { TEC_ENUM_CASE(FeedbackResetReason, Startup); TEC_ENUM_CASE(FeedbackResetReason, Manual); TEC_ENUM_CASE(FeedbackResetReason, ConfigurationChanged); TEC_ENUM_CASE(FeedbackResetReason, SourceChanged); TEC_ENUM_CASE(FeedbackResetReason, IdleTimeout); TEC_ENUM_CASE(FeedbackResetReason, ClockChanged); } return "Unknown"; } const char* toString(FeedbackState v) noexcept { switch (v) { TEC_ENUM_CASE(FeedbackState, Idle); TEC_ENUM_CASE(FeedbackState, Holding); TEC_ENUM_CASE(FeedbackState, Adjusting); TEC_ENUM_CASE(FeedbackState, Limited); TEC_ENUM_CASE(FeedbackState, Invalid); } return "Unknown"; } const char* toString(State v) noexcept { switch (v) { TEC_ENUM_CASE(State, CAPTURE); TEC_ENUM_CASE(State, TRACK); TEC_ENUM_CASE(State, LOST); } return "Unknown"; } const char* toString(ResetReason v) noexcept { switch (v) { TEC_ENUM_CASE(ResetReason, Startup); TEC_ENUM_CASE(ResetReason, Manual); TEC_ENUM_CASE(ResetReason, NewBusinessCycle); TEC_ENUM_CASE(ResetReason, ClockChanged); TEC_ENUM_CASE(ResetReason, ConfigurationChanged); TEC_ENUM_CASE(ResetReason, IdleTimeout); TEC_ENUM_CASE(ResetReason, HistoryExpired); } return "Unknown"; } const char* toString(MeterStatus v) noexcept { switch (v) { TEC_ENUM_CASE(MeterStatus, Valid); TEC_ENUM_CASE(MeterStatus, NoTarget); TEC_ENUM_CASE(MeterStatus, EmptyImage); TEC_ENUM_CASE(MeterStatus, UnsupportedImage); TEC_ENUM_CASE(MeterStatus, InvalidRoi); TEC_ENUM_CASE(MeterStatus, EmptyRoi); TEC_ENUM_CASE(MeterStatus, InsufficientPixels); TEC_ENUM_CASE(MeterStatus, InvalidConfiguration); } return "Unknown"; } const char* toString(PredictionReason v) noexcept { switch (v) { TEC_ENUM_CASE(PredictionReason, Available); TEC_ENUM_CASE(PredictionReason, Disabled); TEC_ENUM_CASE(PredictionReason, ModelNotConfirmed); TEC_ENUM_CASE(PredictionReason, CurrentMeasurementInvalid); TEC_ENUM_CASE(PredictionReason, InsufficientHistory); TEC_ENUM_CASE(PredictionReason, MissingHorizon); TEC_ENUM_CASE(PredictionReason, InvalidHorizon); TEC_ENUM_CASE(PredictionReason, DarkOrSaturated); TEC_ENUM_CASE(PredictionReason, ResidualTooLarge); TEC_ENUM_CASE(PredictionReason, OutOfRange); TEC_ENUM_CASE(PredictionReason, InvalidNumbers); } return "Unknown"; } const char* toString(Reason v) noexcept { switch (v) { TEC_ENUM_CASE(Reason, Initialized); TEC_ENUM_CASE(Reason, Ready); TEC_ENUM_CASE(Reason, InvalidConfiguration); TEC_ENUM_CASE(Reason, InvalidTimestamp); TEC_ENUM_CASE(Reason, DuplicateFrame); TEC_ENUM_CASE(Reason, OutOfOrderFrame); TEC_ENUM_CASE(Reason, PreCaptureRequired); TEC_ENUM_CASE(Reason, InvalidActualExposure); TEC_ENUM_CASE(Reason, ConfirmingTarget); TEC_ENUM_CASE(Reason, TrackingActivated); TEC_ENUM_CASE(Reason, InvalidMetering); TEC_ENUM_CASE(Reason, Deadband); TEC_ENUM_CASE(Reason, Hysteresis); TEC_ENUM_CASE(Reason, ConfirmingError); TEC_ENUM_CASE(Reason, ProportionalAdjustment); TEC_ENUM_CASE(Reason, PiAdjustment); TEC_ENUM_CASE(Reason, ExposureLimit); TEC_ENUM_CASE(Reason, TargetMissing); TEC_ENUM_CASE(Reason, EnteredLost); TEC_ENUM_CASE(Reason, Frozen); TEC_ENUM_CASE(Reason, CaptureAfterFreeze); TEC_ENUM_CASE(Reason, RecoveryNotAllowed); TEC_ENUM_CASE(Reason, RecoveryReferenceMissing); TEC_ENUM_CASE(Reason, RecoveryNotNeeded); TEC_ENUM_CASE(Reason, RecoverySuggested); TEC_ENUM_CASE(Reason, NotRecovered); } return "Unknown"; } #undef TEC_ENUM_CASE } // namespace target_exposure