#include "fgc/SettleRule.h" #include #include #include #include #include #include #include #include using namespace fgc; namespace { FrameStats frame(double b, double e = 20000, double g = 15) { static uint64_t seq = 0; return FrameStats{++seq, b, e, g}; } struct Row { double t, exposure, gain, brightness; }; // The 24 auto-exposure steps recorded on towers 121 and 111 (tests/data/ae_steps.csv). std::map> recordedSteps() { std::map> runs; std::ifstream f(std::string(FGC_SOURCE_DIR) + "/tests/data/ae_steps.csv"); std::string line; while (std::getline(f, line)) { if (line.empty() || line[0] == '#') continue; std::stringstream ss(line); std::string field; std::vector v; while (std::getline(ss, field, ',')) v.push_back(field); if (v.size() != 8) continue; runs[std::stoi(v[0])].push_back({std::stod(v[4]), std::stod(v[5]), std::stod(v[6]), std::stod(v[7])}); } return runs; } } // namespace TEST_CASE("settled: in the camera's band and steady for 3 frames") { SettleRule r; // band 50 +- 5 % of 255 = 114.75 .. 140.25 CHECK(r.add(frame(120)) == SettleRule::Verdict::Waiting); CHECK(r.add(frame(121)) == SettleRule::Verdict::Waiting); CHECK(r.add(frame(121.5)) == SettleRule::Verdict::Settled); } TEST_CASE("not settled while brightness is still moving through the band") { SettleRule r; r.add(frame(139)); r.add(frame(131)); // -6%: still coming down CHECK(r.add(frame(124)) == SettleRule::Verdict::Waiting); CHECK(r.add(frame(122)) == SettleRule::Verdict::Waiting); // 124 -> 122 steady, 131 -> 124 not CHECK(r.add(frame(122)) == SettleRule::Verdict::Settled); } TEST_CASE("a pause outside the band is not settled (auto-exposure stalls mid-way)") { SettleRule r; for (int i = 0; i < 10; ++i) CHECK(r.add(frame(157, 20000, 19.6)) == SettleRule::Verdict::Waiting); } TEST_CASE("steady but too dark at full exposure and gain: waiting cannot help") { SettleRule::Params p; p.exposure_max_us = 20000; p.gain_max_db = 24; SettleRule r(p); r.add(frame(40, 20000, 24)); r.add(frame(40.2, 20000, 24)); CHECK(r.add(frame(40.1, 20000, 24)) == SettleRule::Verdict::AtLimit); // Below the limits it is just slow, not stuck. SettleRule s(p); s.add(frame(40, 20000, 12)); s.add(frame(40, 20000, 12)); CHECK(s.add(frame(40, 20000, 12)) == SettleRule::Verdict::Waiting); } TEST_CASE("steady but too bright at minimum exposure and gain") { SettleRule::Params p; p.exposure_min_us = 82; p.gain_min_db = 0; SettleRule r(p); r.add(frame(200, 82, 0)); r.add(frame(200, 82, 0)); CHECK(r.add(frame(200, 82, 0)) == SettleRule::Verdict::AtLimit); } TEST_CASE("a frame without brightness starts the count again") { SettleRule r; r.add(frame(120)); r.add(frame(120)); CHECK(r.add(frame(-1)) == SettleRule::Verdict::Waiting); CHECK(r.add(frame(120)) == SettleRule::Verdict::Waiting); CHECK(r.add(frame(120)) == SettleRule::Verdict::Waiting); CHECK(r.add(frame(120)) == SettleRule::Verdict::Settled); } TEST_CASE("the band follows the camera's target and tolerance") { SettleRule::Params p; p.target_pct = 40; p.tolerance_pct = 10; SettleRule r(p); CHECK(r.bandLow() == doctest::Approx(76.5)); CHECK(r.bandHigh() == doctest::Approx(127.5)); } TEST_CASE("replayed on the 24 recorded exposure steps, it keeps a good frame at least 21 times") { const auto runs = recordedSteps(); REQUIRE(runs.size() == 24); int correct = 0, early = 0, never = 0; for (const auto& [id, rows] : runs) { // Where the run finally settles: median brightness over its last 2 s. std::vector tail; for (const auto& r : rows) if (r.t >= rows.back().t - 2.0) tail.push_back(r.brightness); std::sort(tail.begin(), tail.end()); const double final_level = tail[tail.size() / 2]; SettleRule rule; bool fired = false; for (const auto& r : rows) { if (rule.add(FrameStats{0, r.brightness, r.exposure, r.gain}) != SettleRule::Verdict::Waiting) { fired = true; (std::fabs(r.brightness / final_level - 1.0) <= 0.10 ? correct : early)++; break; } } never += !fired; } INFO("correct " << correct << ", early " << early << ", never " << never); CHECK(correct >= 21); CHECK(never == 0); }