fwt_software/tests/test_tariscamerasource.cpp

386 lines
14 KiB
C++

#include "fgc/TarisCameraSource.h"
#include <doctest/doctest.h>
#include <atomic>
#include <chrono>
#include <cstring>
#include <mutex>
#include <string>
#include <thread>
#include <vector>
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<std::string> 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<std::string> 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<uint16_t> px;
int64_t image_num = 0; // ours; the camera reports 0 for every frame
int lut_calls = 0; // the first LUTs after connecting are empty
uint32_t w = 8, h = 4;
Ret no_frame = kErrNotFound; // what getdata says before any frame
std::atomic<bool> frozen{false}; // stop producing frames (stall)
void push(uint16_t v, uint16_t hot = 0) {
std::lock_guard<std::mutex> l(m);
px.assign(static_cast<size_t>(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<size_t>(len));
std::memcpy(buf, s.data(), std::min<size_t>(s.size(), static_cast<size_t>(len) - 1));
}
Ret fInit() { std::lock_guard<std::mutex> 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<std::mutex> l(g->m);
*n = static_cast<int32_t>(g->search.size());
return kSuccess;
}
Ret fConnect(Handle, char* s) {
std::lock_guard<std::mutex> 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; g->lut_calls = 0; }
return ok ? kSuccess : kTypeErr | 0x10;
}
Ret fDisconnect(Handle) { return kSuccess; }
Ret fStart(Handle, int32_t) { std::lock_guard<std::mutex> l(g->m); g->grabbing = true; return kSuccess; }
Ret fStop(Handle, int32_t) { std::lock_guard<std::mutex> l(g->m); g->grabbing = false; return kSuccess; }
Ret fGetParam(Handle, uint32_t what, void* data, uint32_t type) {
std::lock_guard<std::mutex> l(g->m);
if (what == param::kSearchString && type == kTypeIdxString) {
auto* p = static_cast<IdxString*>(data);
if (p->index < 0 || p->index >= static_cast<int>(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<IdxInt32*>(data)->value = g->cal;
return kSuccess;
}
if (what == param::kCalibCount && type == kTypeInt32) { *static_cast<int32_t*>(data) = 2; return kSuccess; }
if (what == param::kCalibChangeInProgress && type == kTypeInt32) { *static_cast<int32_t*>(data) = 0; return kSuccess; }
if (what == param::kCalibName && type == kTypeIdxString) {
auto* p = static_cast<IdxString*>(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<String*>(data);
copyText("TARISirmini", p->text, p->len);
return kSuccess;
}
if (what == param::kCameraTempHousing && type == kTypeSingle) { *static_cast<float*>(data) = 308.15f; return kSuccess; }
return kErrNotSupported;
}
Ret fSetParam(Handle, uint32_t what, void* data, uint32_t type) {
std::lock_guard<std::mutex> l(g->m);
if (what == param::kCalibActive && type == kTypeIdxInt32) {
g->cal = static_cast<IdxInt32*>(data)->value;
++g->calib_sets;
return kSuccess;
}
if (what == param::kFramerateHz && type == kTypeSingle) { g->rate = *static_cast<float*>(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<uint16_t> px;
uint32_t w, h;
int64_t num;
int cal;
};
Ret fGetData(Handle, int32_t what, Handle* mem) {
std::lock_guard<std::mutex> l(g->m);
if (what != kMemIrDigitFrame || !g->grabbing || g->px.empty() || g->frozen) return g->no_frame;
*mem = reinterpret_cast<Handle>(new Mem{g->px, g->w, g->h, g->image_num, g->cal});
return kSuccess;
}
Ret fMemFree(Handle* mem) { delete reinterpret_cast<Mem*>(*mem); *mem = 0; return kSuccess; }
Ret fDim(Handle mem, uint32_t* w, uint32_t* h, uint32_t* t) {
auto* m = reinterpret_cast<Mem*>(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*>(mem);
*p = reinterpret_cast<Handle>(m->px.data());
*bytes = static_cast<int32_t>(m->px.size() * 2);
return kSuccess;
}
Ret fInfo(Handle mem, void* info) {
// As the TarisIR mini: no frame counter, but each frame has its own time.
auto* mi = static_cast<MemInfo*>(info);
mi->image_num = 0;
mi->timestamp_ms = 200.0 * static_cast<double>(reinterpret_cast<Mem*>(mem)->num);
mi->missed_packets = 1;
return kSuccess;
}
Ret fLut(Handle mem, void* lut, float, float, float, float) {
const double kpc = reinterpret_cast<Mem*>(mem)->cal == 1 ? 0.02 : 0.01;
auto* l = static_cast<float*>(lut);
{
std::lock_guard<std::mutex> lk(g->m);
if (++g->lut_calls <= 2) { // still empty, and "ok", as on the tower
for (int i = 0; i < 65536; ++i) l[i] = 0.f;
return kSuccess;
}
}
for (int i = 0; i < 65536; ++i) l[i] = static_cast<float>(i * kpc);
return kSuccess;
}
std::shared_ptr<const Api> fakeApi() {
auto a = std::make_shared<Api>();
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 <class F> 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<Frame> 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<std::mutex> 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<std::mutex> 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)); // documented, then measured
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<std::mutex> 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<std::mutex> 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<std::mutex> 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);
}