162 lines
4.9 KiB
C++
162 lines
4.9 KiB
C++
#include <doctest/doctest.h>
|
|
|
|
#include "fgc/CaptureScheduler.h"
|
|
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
using namespace fgc;
|
|
|
|
namespace {
|
|
|
|
struct FakeMotor : IMotorController {
|
|
MotorTelemetry tel;
|
|
std::vector<std::string> 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);
|
|
}
|