240 lines
7.8 KiB
C++
240 lines
7.8 KiB
C++
#include "fgc/CompositeCameraSource.h"
|
|
|
|
#include <doctest/doctest.h>
|
|
|
|
#include <chrono>
|
|
#include <stdexcept>
|
|
#include <thread>
|
|
#include <vector>
|
|
|
|
using namespace fgc;
|
|
|
|
namespace {
|
|
|
|
// A source of `count` cameras whose trigger() delivers one frame per camera,
|
|
// stamped `ts`, when `ready` - and reports nothing otherwise.
|
|
struct FakeSource : ICameraSource {
|
|
int count;
|
|
long long ts;
|
|
bool counts_after_open = false; // as Vimba: no cameras until open()
|
|
bool opened = false;
|
|
bool ready = true;
|
|
bool throws_on_open = false;
|
|
int triggers = 0;
|
|
bool started = false;
|
|
bool frame_ready = true; // frameReady()
|
|
int moves = 0;
|
|
std::string why_offline;
|
|
FrameCallback cb;
|
|
|
|
FakeSource(int n, long long t) : count(n), ts(t) {}
|
|
|
|
void open() override {
|
|
if (throws_on_open) throw std::runtime_error("no camera");
|
|
opened = true;
|
|
}
|
|
void close() override {}
|
|
void start() override { started = true; }
|
|
void stop() override { started = false; }
|
|
void moving() override { ++moves; }
|
|
bool frameReady() override { return frame_ready; }
|
|
bool trigger() override {
|
|
++triggers;
|
|
if (!ready) return false;
|
|
for (int i = 0; i < count; ++i) {
|
|
Frame f;
|
|
f.cam_id = i;
|
|
f.timestamp_ms = ts + i;
|
|
cb(f);
|
|
}
|
|
return true;
|
|
}
|
|
void setFrameCallback(FrameCallback c) override { cb = std::move(c); }
|
|
int cameraCount() const override { return counts_after_open && !opened ? 0 : count; }
|
|
bool readFeature(int i, const std::string& name, double& v) override {
|
|
v = i * 100 + ts;
|
|
return name == "X";
|
|
}
|
|
std::string offline(int) override { return why_offline; }
|
|
std::vector<CameraDeviceInfo> deviceInfo() override {
|
|
std::vector<CameraDeviceInfo> out(count);
|
|
for (int i = 0; i < count; ++i) out[i].id = std::to_string(ts) + ":" + std::to_string(i);
|
|
return out;
|
|
}
|
|
bool grabFrame(int i, Frame& out, int) override {
|
|
out = Frame{};
|
|
out.cam_id = i;
|
|
out.timestamp_ms = ts + 50;
|
|
return true;
|
|
}
|
|
};
|
|
|
|
struct Rig {
|
|
FakeSource* colour = nullptr;
|
|
FakeSource* thermal = nullptr;
|
|
std::unique_ptr<CompositeCameraSource> src;
|
|
std::vector<Frame> frames;
|
|
|
|
explicit Rig(int colour_count = 1) {
|
|
auto c = std::make_unique<FakeSource>(colour_count, 1000);
|
|
auto t = std::make_unique<FakeSource>(1, 5000);
|
|
colour = c.get();
|
|
thermal = t.get();
|
|
std::vector<std::unique_ptr<ICameraSource>> v;
|
|
v.push_back(std::move(c));
|
|
v.push_back(std::move(t));
|
|
src = std::make_unique<CompositeCameraSource>(std::move(v));
|
|
src->setFrameCallback([this](const Frame& f) { frames.push_back(f); });
|
|
}
|
|
};
|
|
|
|
} // namespace
|
|
|
|
TEST_CASE("the composite numbers the cameras leader first, and counts them all") {
|
|
Rig r(2);
|
|
CHECK(r.src->cameraCount() == 3);
|
|
REQUIRE(r.src->trigger());
|
|
REQUIRE(r.frames.size() == 3);
|
|
CHECK(r.frames[0].cam_id == 0);
|
|
CHECK(r.frames[1].cam_id == 1);
|
|
CHECK(r.frames[2].cam_id == 2); // the thermal camera
|
|
}
|
|
|
|
TEST_CASE("every frame keeps the time it arrived, the follower's too") {
|
|
Rig r;
|
|
REQUIRE(r.src->trigger());
|
|
REQUIRE(r.frames.size() == 2);
|
|
CHECK(r.frames[0].timestamp_ms == 1000);
|
|
CHECK(r.frames[1].timestamp_ms == 5000); // its own, not the leader's
|
|
}
|
|
|
|
TEST_CASE("the followers are asked only once the leader has delivered") {
|
|
Rig r;
|
|
r.colour->ready = false;
|
|
CHECK_FALSE(r.src->trigger());
|
|
CHECK_FALSE(r.src->trigger());
|
|
CHECK(r.thermal->triggers == 0); // no thermal picture without its colour one
|
|
CHECK(r.frames.empty());
|
|
r.colour->ready = true;
|
|
CHECK(r.src->trigger());
|
|
CHECK(r.thermal->triggers == 1);
|
|
}
|
|
|
|
TEST_CASE("a follower with nothing to give costs only its own picture") {
|
|
Rig r;
|
|
r.thermal->ready = false;
|
|
CHECK(r.src->trigger());
|
|
REQUIRE(r.frames.size() == 1);
|
|
CHECK(r.frames[0].cam_id == 0);
|
|
}
|
|
|
|
TEST_CASE("calls about one camera reach the source that has it") {
|
|
Rig r(2);
|
|
double v = 0;
|
|
REQUIRE(r.src->readFeature(1, "X", v));
|
|
CHECK(v == 1100); // colour source, its camera 1
|
|
REQUIRE(r.src->readFeature(2, "X", v));
|
|
CHECK(v == 5000); // thermal source, its camera 0
|
|
CHECK_FALSE(r.src->readFeature(3, "X", v));
|
|
|
|
r.thermal->why_offline = "unplugged";
|
|
CHECK(r.src->offline(0).empty());
|
|
CHECK(r.src->offline(2) == "unplugged");
|
|
|
|
const auto d = r.src->deviceInfo();
|
|
REQUIRE(d.size() == 3);
|
|
CHECK(d[2].id == "5000:0");
|
|
|
|
// A grab outside a trigger keeps its own time, under the global index.
|
|
Frame f;
|
|
REQUIRE(r.src->grabFrame(2, f, 10));
|
|
CHECK(f.cam_id == 2);
|
|
CHECK(f.timestamp_ms == 5050);
|
|
}
|
|
|
|
TEST_CASE("the indices are the configured ones, also before the leader has opened") {
|
|
// On tower 111 the thermal frame came out as camera 0 - "RGB", overwriting
|
|
// the colour file of the same time - because Vimba counts no camera before
|
|
// open() and the offsets were taken from that.
|
|
auto c = std::make_unique<FakeSource>(1, 1000);
|
|
auto t = std::make_unique<FakeSource>(1, 5000);
|
|
c->counts_after_open = true;
|
|
FakeSource* colour = c.get();
|
|
std::vector<std::unique_ptr<ICameraSource>> v;
|
|
v.push_back(std::move(c));
|
|
v.push_back(std::move(t));
|
|
CompositeCameraSource src(std::move(v), {1, 1});
|
|
std::vector<Frame> frames;
|
|
src.setFrameCallback([&](const Frame& f) { frames.push_back(f); });
|
|
CHECK(src.cameraCount() == 1); // what is open: the thermal camera only
|
|
src.open();
|
|
CHECK(colour->opened);
|
|
CHECK(src.cameraCount() == 2);
|
|
REQUIRE(src.trigger());
|
|
REQUIRE(frames.size() == 2);
|
|
CHECK(frames[0].cam_id == 0);
|
|
CHECK(frames[1].cam_id == 1);
|
|
CHECK(src.offline(1).empty());
|
|
CHECK_THROWS(CompositeCameraSource(std::vector<std::unique_ptr<ICameraSource>>{}, {}));
|
|
}
|
|
|
|
TEST_CASE("a picture waits a moment for a follower between frames, then goes without it") {
|
|
auto c = std::make_unique<FakeSource>(1, 1000);
|
|
auto t = std::make_unique<FakeSource>(1, 5000);
|
|
FakeSource* colour = c.get();
|
|
FakeSource* thermal = t.get();
|
|
std::vector<std::unique_ptr<ICameraSource>> v;
|
|
v.push_back(std::move(c));
|
|
v.push_back(std::move(t));
|
|
CompositeCameraSource src(std::move(v), {1, 1}, 60);
|
|
std::vector<Frame> frames;
|
|
src.setFrameCallback([&](const Frame& f) { frames.push_back(f); });
|
|
|
|
// The thermal camera is in its NUC: the picture is held, the leader not asked.
|
|
thermal->frame_ready = false;
|
|
CHECK_FALSE(src.trigger());
|
|
CHECK(colour->triggers == 0);
|
|
CHECK_FALSE(src.frameReady());
|
|
// It comes back within the wait: one whole pair.
|
|
thermal->frame_ready = true;
|
|
REQUIRE(src.trigger());
|
|
CHECK(frames.size() == 2);
|
|
|
|
// It does not come back: after the wait the colour picture goes alone.
|
|
frames.clear();
|
|
thermal->frame_ready = false;
|
|
CHECK_FALSE(src.trigger());
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(80));
|
|
REQUIRE(src.trigger());
|
|
CHECK(colour->triggers == 2);
|
|
}
|
|
|
|
TEST_CASE("every source hears that the gimbal moves") {
|
|
Rig r;
|
|
r.src->moving();
|
|
CHECK(r.colour->moves == 1);
|
|
CHECK(r.thermal->moves == 1);
|
|
}
|
|
|
|
TEST_CASE("start and stop reach every source") {
|
|
Rig r;
|
|
r.src->start();
|
|
CHECK(r.colour->started);
|
|
CHECK(r.thermal->started);
|
|
r.src->stop();
|
|
CHECK_FALSE(r.thermal->started);
|
|
}
|
|
|
|
TEST_CASE("a follower that will not open does not stop the run; the leader does") {
|
|
{
|
|
Rig r;
|
|
r.thermal->throws_on_open = true;
|
|
CHECK_NOTHROW(r.src->open());
|
|
}
|
|
{
|
|
Rig r;
|
|
r.colour->throws_on_open = true;
|
|
CHECK_THROWS(r.src->open());
|
|
}
|
|
}
|