CommonLibraries/src/config.cpp
Ben 11236718ea chore: preserve existing library source changes for MatchVerify migration
Isolated source snapshot; original checkout and index unchanged. This migration does not claim new build or hardware validation.
2026-10-20 23:59:35 +08:00

190 lines
12 KiB
C++

#include "internal.hpp"
#include <cmath>
#include <tuple>
namespace target_exposure {
std::vector<std::string> detail::validateController(const ControllerConfig& k) {
std::vector<std::string> 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<std::string> detail::validateMetering(const MeteringConfig& m) {
std::vector<std::string> 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<std::string> validate(const Config& c) {
std::vector<std::string> 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<std::string> validate(const ContinuousConfig& c) {
std::vector<std::string> 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