#include "fgc/TarisCameraSource.h" #include #include #include #include #include #include #include #include using namespace fgc; using namespace fgc::irbgrab; namespace { // A stand-in for the IRBGRAB SDK and one TarisIR mini behind it. The source // calls it from its worker thread; the test changes it from its own. struct FakeSdk { std::mutex m; std::vector search{"SN:2017744|00:0e:c6:aa:bb:cc|192.168.0.200#0"}; std::string accept = "#0"; // connect succeeds for strings ending like this std::vector connect_attempts; int cal = 0; int calib_sets = 0; float rate = 0.f; bool grabbing = false; int inits = 0; int connects = 0; // The newest frame: counts, and the camera's number for it. std::vector px; int64_t image_num = 0; uint32_t w = 8, h = 4; Ret no_frame = kErrNotFound; // what getdata says before any frame std::atomic frozen{false}; // stop producing frames (stall) void push(uint16_t v, uint16_t hot = 0) { std::lock_guard l(m); px.assign(static_cast(w) * h, v); if (hot) px[5] = hot; ++image_num; } }; FakeSdk* g = nullptr; void copyText(const std::string& s, char* buf, int len) { std::memset(buf, 0, static_cast(len)); std::memcpy(buf, s.data(), std::min(s.size(), static_cast(len) - 1)); } Ret fInit() { std::lock_guard l(g->m); ++g->inits; return kSuccess; } void fUninit() {} Ret fVersion(char* b, int n) { copyText("4.3.44", b, n); return kSuccess; } Ret fTypes(char* b, int n) { copyText("Simulator;TARISirmini", b, n); return kSuccess; } Ret fCreate(Handle* d, int32_t type, char*) { *d = type == 1 ? 42 : 0; return type == 1 ? kSuccess : kErrNotFound; } Ret fFree(Handle* d) { *d = 0; return kSuccess; } Ret fSearch(Handle, int32_t* n) { std::lock_guard l(g->m); *n = static_cast(g->search.size()); return kSuccess; } Ret fConnect(Handle, char* s) { std::lock_guard l(g->m); const std::string str = s; g->connect_attempts.push_back(str); const bool ok = str.size() >= g->accept.size() && str.compare(str.size() - g->accept.size(), g->accept.size(), g->accept) == 0; if (ok) ++g->connects; return ok ? kSuccess : kTypeErr | 0x10; } Ret fDisconnect(Handle) { return kSuccess; } Ret fStart(Handle, int32_t) { std::lock_guard l(g->m); g->grabbing = true; return kSuccess; } Ret fStop(Handle, int32_t) { std::lock_guard l(g->m); g->grabbing = false; return kSuccess; } Ret fGetParam(Handle, uint32_t what, void* data, uint32_t type) { std::lock_guard l(g->m); if (what == param::kSearchString && type == kTypeIdxString) { auto* p = static_cast(data); if (p->index < 0 || p->index >= static_cast(g->search.size())) return kErrNotFound; copyText(g->search[p->index], p->value.text, p->value.len); return kSuccess; } if (what == param::kCalibActive && type == kTypeIdxInt32) { static_cast(data)->value = g->cal; return kSuccess; } if (what == param::kCalibCount && type == kTypeInt32) { *static_cast(data) = 2; return kSuccess; } if (what == param::kCalibChangeInProgress && type == kTypeInt32) { *static_cast(data) = 0; return kSuccess; } if (what == param::kCalibName && type == kTypeIdxString) { auto* p = static_cast(data); copyText(p->index == 0 ? "FF (-40-120C) #4101" : "FF (0-600C) #4102", p->value.text, p->value.len); return kSuccess; } if (what == param::kCameraName && type == kTypeString) { auto* p = static_cast(data); copyText("TARISirmini", p->text, p->len); return kSuccess; } if (what == param::kCameraTempHousing && type == kTypeSingle) { *static_cast(data) = 308.15f; return kSuccess; } return kErrNotSupported; } Ret fSetParam(Handle, uint32_t what, void* data, uint32_t type) { std::lock_guard l(g->m); if (what == param::kCalibActive && type == kTypeIdxInt32) { g->cal = static_cast(data)->value; ++g->calib_sets; return kSuccess; } if (what == param::kFramerateHz && type == kTypeSingle) { g->rate = *static_cast(data); return kSuccess; } if (what == param::kPollingEnable) return kSuccess; return kErrNotSupported; } // A memory object is a copy of the newest frame. struct Mem { std::vector px; uint32_t w, h; int64_t num; int cal; }; Ret fGetData(Handle, int32_t what, Handle* mem) { std::lock_guard l(g->m); if (what != kMemIrDigitFrame || !g->grabbing || g->px.empty() || g->frozen) return g->no_frame; *mem = reinterpret_cast(new Mem{g->px, g->w, g->h, g->image_num, g->cal}); return kSuccess; } Ret fMemFree(Handle* mem) { delete reinterpret_cast(*mem); *mem = 0; return kSuccess; } Ret fDim(Handle mem, uint32_t* w, uint32_t* h, uint32_t* t) { auto* m = reinterpret_cast(mem); *w = m->w; *h = m->h; *t = 0; return kSuccess; } Ret fPtr(Handle mem, Handle* p, int32_t* bytes) { auto* m = reinterpret_cast(mem); *p = reinterpret_cast(m->px.data()); *bytes = static_cast(m->px.size() * 2); return kSuccess; } Ret fInfo(Handle mem, void* info) { auto* mi = static_cast(info); mi->image_num = reinterpret_cast(mem)->num; mi->missed_packets = 1; return kSuccess; } Ret fLut(Handle mem, void* lut, float, float, float, float) { const double kpc = reinterpret_cast(mem)->cal == 1 ? 0.02 : 0.01; auto* l = static_cast(lut); for (int i = 0; i < 65536; ++i) l[i] = static_cast(i * kpc); return kSuccess; } std::shared_ptr fakeApi() { auto a = std::make_shared(); a->dll_init = fInit; a->dll_uninit = fUninit; a->dll_version = fVersion; a->dll_devicetypenames = fTypes; a->dev_create = fCreate; a->dev_free = fFree; a->dev_search = fSearch; a->dev_connect = fConnect; a->dev_disconnect = fDisconnect; a->dev_startgrab = fStart; a->dev_stopgrab = fStop; a->dev_getparam = fGetParam; a->dev_setparam = fSetParam; a->dev_getdata = fGetData; a->mem_free = fMemFree; a->mem_getdimension = fDim; a->mem_getdataptr = fPtr; a->mem_getinfo = fInfo; a->mem_getdvlut = fLut; return a; } TarisCameraSource::Options fastOptions() { TarisCameraSource::Options o; o.serial = "2017744"; o.frame_rate_hz = 50; // polls every 10 ms o.retry_min_ms = 20; o.retry_max_ms = 40; o.stall_ms = 2000; return o; } // Each call of pred() counts: trigger() is not idempotent. template bool waitFor(F pred, int ms = 3000) { const auto end = std::chrono::steady_clock::now() + std::chrono::milliseconds(ms); for (;;) { if (pred()) return true; if (std::chrono::steady_clock::now() >= end) return false; std::this_thread::sleep_for(std::chrono::milliseconds(5)); } } struct Harness { FakeSdk sdk; TarisCameraSource cam; std::mutex m; std::vector frames; explicit Harness(TarisCameraSource::Options o = fastOptions(), bool net_raw = true) : cam((g = &sdk, o), fakeApi(), [net_raw] { return net_raw; }) { cam.setFrameCallback([this](const Frame& f) { std::lock_guard l(m); frames.push_back(f); }); } ~Harness() { cam.close(); g = nullptr; } bool online() { return waitFor([this] { return cam.offline(0).empty(); }); } // Trigger until a frame comes or the time is up. bool shot(int ms = 2000) { return waitFor([this] { return cam.trigger(); }, ms); } }; } // namespace TEST_CASE("the thermal camera connects, trying adapters until one answers") { Harness t; t.sdk.accept = "#3"; // the adapter index has moved since the search t.cam.open(); t.cam.start(); t.sdk.push(29315, 57315); REQUIRE(t.online()); std::lock_guard l(t.sdk.m); REQUIRE(t.sdk.connect_attempts.size() >= 4); CHECK(t.sdk.connect_attempts.front() == "SN:2017744|00:0e:c6:aa:bb:cc|192.168.0.200#0"); CHECK(t.sdk.connect_attempts.back().substr(t.sdk.connect_attempts.back().size() - 2) == "#3"); CHECK(t.sdk.rate == doctest::Approx(50.f)); CHECK(t.sdk.grabbing); CHECK(t.sdk.calib_sets == 0); // already on the wanted profile } TEST_CASE("a fresh frame is delivered once, as counts with their scale") { Harness t; t.cam.open(); t.cam.start(); t.sdk.push(29315, 57315); REQUIRE(t.shot()); CHECK_FALSE(t.cam.trigger()); // the same frame is not handed out twice REQUIRE(t.frames.size() == 1); const Frame& f = t.frames[0]; CHECK(f.layout == PixelLayout::Mono16); CHECK(f.width == 8); CHECK(f.height == 4); REQUIRE(f.data.size() == 64); uint16_t px5 = 0; std::memcpy(&px5, f.data.data() + 10, 2); CHECK(px5 == 57315); REQUIRE(f.thermal); CHECK(f.thermal->cal == 0); CHECK(f.thermal->kelvin_per_count == doctest::Approx(0.01)); // measured from the LUT CHECK(f.thermal->missed_packets == 1); t.sdk.push(29400); CHECK(t.shot()); double v = 0; CHECK(t.cam.readFeature(0, "CalibProfile", v)); CHECK(v == 0); CHECK(waitFor([&] { return t.cam.readFeature(0, "HousingTemperature", v); }, 7000)); CHECK(v == doctest::Approx(35.0)); } TEST_CASE("the zero frames after starting are never delivered") { Harness t; t.cam.open(); t.cam.start(); t.sdk.push(0); std::this_thread::sleep_for(std::chrono::milliseconds(100)); CHECK_FALSE(t.cam.trigger()); t.sdk.push(29315); CHECK(t.shot()); } TEST_CASE("a frame older than max_frame_age_ms is not kept") { auto o = fastOptions(); o.max_frame_age_ms = 50; Harness t(o); t.cam.open(); t.cam.start(); t.sdk.push(29315); REQUIRE(t.online()); t.sdk.frozen = true; // no newer frame arrives std::this_thread::sleep_for(std::chrono::milliseconds(150)); CHECK_FALSE(t.cam.trigger()); } TEST_CASE("delivery stops while stopped, the camera keeps grabbing") { Harness t; t.cam.open(); t.sdk.push(29315); REQUIRE(t.online()); CHECK_FALSE(t.cam.trigger()); // not started t.cam.start(); CHECK(t.cam.trigger()); t.cam.stop(); t.sdk.push(29400); std::this_thread::sleep_for(std::chrono::milliseconds(50)); CHECK_FALSE(t.cam.trigger()); std::lock_guard l(t.sdk.m); CHECK(t.sdk.grabbing); } TEST_CASE("the configured calibration profile is set on connect") { auto o = fastOptions(); o.calib_profile = 1; Harness t(o); t.cam.open(); t.cam.start(); t.sdk.push(14658); REQUIRE(t.shot()); CHECK(t.sdk.calib_sets == 1); REQUIRE(t.frames[0].thermal); CHECK(t.frames[0].thermal->cal == 1); CHECK(t.frames[0].thermal->kelvin_per_count == doctest::Approx(0.02)); } TEST_CASE("switching the profile while running changes the scale of later frames") { Harness t; t.cam.open(); t.cam.start(); t.sdk.push(29315); REQUIRE(t.shot()); REQUIRE(t.cam.setFeature(0, "CalibProfile", "1")); CHECK_FALSE(t.cam.setFeature(0, "CalibProfile", "7")); // the camera has two CHECK_FALSE(t.cam.setFeature(0, "CalibProfile", "x")); t.sdk.push(14658); REQUIRE(t.shot()); REQUIRE(t.frames.back().thermal); CHECK(t.frames.back().thermal->cal == 1); CHECK(t.frames.back().thermal->kelvin_per_count == doctest::Approx(0.02)); } TEST_CASE("a camera not on the network is offline, says why, and is found later") { Harness t; t.sdk.search = {"SN:999|00:00:00:00:00:01|192.168.0.201#0"}; t.cam.open(); t.cam.start(); t.sdk.push(29315); CHECK(waitFor([&] { return t.cam.offline(0).find("not found") != std::string::npos; })); CHECK_FALSE(t.cam.trigger()); { std::lock_guard l(t.sdk.m); t.sdk.search = {"SN:2017744|00:0e:c6:aa:bb:cc|192.168.0.200#0"}; } CHECK(t.online()); CHECK(t.shot()); } TEST_CASE("without CAP_NET_RAW the camera stays offline and the SDK is not touched") { Harness t(fastOptions(), false); t.cam.open(); CHECK(waitFor([&] { return t.cam.offline(0).find("cap_net_raw") != std::string::npos; })); CHECK(t.sdk.inits == 0); CHECK_FALSE(t.cam.trigger()); } TEST_CASE("a camera that goes quiet is reconnected") { auto o = fastOptions(); o.stall_ms = 100; Harness t(o); t.cam.open(); t.cam.start(); t.sdk.push(29315); REQUIRE(t.online()); t.sdk.frozen = true; CHECK(waitFor([&] { return t.cam.deviceInfo()[0].restarts >= 1; })); t.sdk.frozen = false; t.sdk.push(29400); CHECK(t.shot()); std::lock_guard l(t.sdk.m); CHECK(t.sdk.connects >= 2); } TEST_CASE("a missing SDK library leaves the camera offline, not the program down") { auto o = fastOptions(); o.library = "libdoes_not_exist_irbgrab.so"; TarisCameraSource cam(o, nullptr, [] { return true; }); cam.open(); CHECK(waitFor([&] { return cam.offline(0).find("cannot load") != std::string::npos; })); CHECK_FALSE(cam.trigger()); cam.close(); } TEST_CASE("the documented scales") { CHECK(TarisCameraSource::expectedKelvinPerCount(0) == 0.01); CHECK(TarisCameraSource::expectedKelvinPerCount(1) == 0.02); CHECK(TarisCameraSource::expectedKelvinPerCount(2) == 0.0); }