#include #include "fgc/CaptureScheduler.h" #include #include using namespace fgc; namespace { struct FakeMotor : IMotorController { MotorTelemetry tel; std::vector cmds; void start() override {} void stop() override {} void sendCommand(const std::string& c) override { cmds.push_back(c); } MotorTelemetry telemetry() override { return tel; } std::string lastDump() override { return ""; } bool connected() const override { return true; } }; struct FakeCamera : ICameraSource { int triggers = 0; bool ok = true; void open() override {} void close() override {} void start() override {} void stop() override {} bool trigger() override { ++triggers; return ok; } void setFrameCallback(FrameCallback) override {} int cameraCount() const override { return 1; } }; struct FakeChannel : IControlChannel { ControlCommand next; int last_status = -1; bool connect() override { return true; } void disconnect() override {} bool connected() const override { return true; } void publishStatus(int c) override { last_status = c; } void publishCamEvent(const CamEvent&) override {} ControlCommand poll() override { ControlCommand c = next; next = {}; return c; } }; // Simple linear map: 10 counts/deg, zero at 0. Yaw clamps -90..90, pitch 0..60. Geometry testGeometry() { Geometry g; g.yaw = {10.0, 0, -90.0, 90.0}; g.pitch = {10.0, 0, 0.0, 60.0}; return g; } // Mark both axes settled at the scheduler's current target. void settleAt(FakeMotor& m, long yaw, long pitch) { m.tel.yaw.xenc = yaw; m.tel.yaw.standstill = true; m.tel.yaw.state = AxisState::Ready; m.tel.pitch.xenc = pitch; m.tel.pitch.standstill = true; m.tel.pitch.state = AxisState::Ready; m.tel.pitch_present = true; } } // namespace TEST_CASE("CaptureScheduler sweeps the scan grid with MOVE + settle + trigger") { long long clock = 0; FakeMotor motor; FakeCamera cam; FakeChannel chan; ScanGrid grid({{0.0, 0.0}, {90.0, 0.0}}); // -> yaw counts 0, then 900 CaptureScheduler sch(motor, cam, chan, 1.0 /*img/s -> 1000ms*/, testGeometry(), grid, [&] { return clock; }); sch.setCaptureActive(true); chan.next.control_code_available = true; chan.next.control_code = 0; // Before the interval elapses: nothing moves, but status is echoed. clock = 500; sch.tick(); CHECK(motor.cmds.empty()); CHECK(chan.last_status == 0); // After the interval: MOVE to the first grid point (0,0) is issued. clock = 1600; sch.tick(); REQUIRE(motor.cmds.size() == 1); CHECK(motor.cmds[0] == "MOVE 0,0"); CHECK(sch.moving()); CHECK(cam.triggers == 0); // not yet settled // Report standstill at the target -> camera fires, cursor advances. settleAt(motor, 0, 0); clock = 1700; sch.tick(); CHECK(cam.triggers == 1); CHECK_FALSE(sch.moving()); // Next interval: MOVE to the second grid point (90 deg -> 900 counts). clock = 2800; sch.tick(); CHECK(motor.cmds.back() == "MOVE 900,0"); } TEST_CASE("CaptureScheduler ControlCode 1 drives yaw to the target heading") { long long clock = 0; FakeMotor motor; FakeCamera cam; FakeChannel chan; ScanGrid grid({{0.0, 0.0}}); CaptureScheduler sch(motor, cam, chan, 1.0, testGeometry(), grid, [&] { return clock; }); sch.setCaptureActive(true); chan.next.control_code_available = true; chan.next.control_code = 1; chan.next.heading_available = true; chan.next.target_heading = "45"; // 45 deg * 10 = 450 counts clock = 1600; sch.tick(); REQUIRE(!motor.cmds.empty()); CHECK(motor.cmds.back() == "MOVE 450,0"); CHECK(sch.controlCode() == 1); } TEST_CASE("CaptureScheduler clamps the target heading to the soft range") { long long clock = 0; FakeMotor motor; FakeCamera cam; FakeChannel chan; ScanGrid grid({{0.0, 0.0}}); CaptureScheduler sch(motor, cam, chan, 1.0, testGeometry(), grid, [&] { return clock; }); sch.setCaptureActive(true); chan.next.control_code_available = true; chan.next.control_code = 1; chan.next.heading_available = true; chan.next.target_heading = "200"; // clamped to yaw max 90 deg -> 900 counts clock = 1600; sch.tick(); CHECK(motor.cmds.back() == "MOVE 900,0"); } TEST_CASE("CaptureScheduler stays idle when capture inactive") { long long clock = 0; FakeMotor motor; FakeCamera cam; FakeChannel chan; ScanGrid grid({{0.0, 0.0}}); CaptureScheduler sch(motor, cam, chan, 1.0, testGeometry(), grid, [&] { return clock; }); // capture not active clock = 5000; sch.tick(); CHECK(motor.cmds.empty()); CHECK(cam.triggers == 0); }