477 lines
17 KiB
C++
477 lines
17 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)
|
|
std::atomic<int> nuc_interval{30}; // the camera's own NUC timer, s
|
|
std::atomic<int> nucs{0}; // NUC_Perform calls
|
|
std::atomic<int> nuc_mode{0};
|
|
|
|
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; }
|
|
if (what == param::kNucInterval && type == kTypeInt32) { *static_cast<int32_t*>(data) = g->nuc_interval; 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;
|
|
if (what == param::kNucInterval && type == kTypeInt32) {
|
|
g->nuc_interval = *static_cast<int32_t*>(data);
|
|
return kSuccess;
|
|
}
|
|
if (what == param::kNucPerform && type == kType2Int32) {
|
|
// As the camera: the call returns when the NUC is done.
|
|
g->nuc_mode = static_cast<Int2*>(data)->value1;
|
|
++g->nucs;
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(20));
|
|
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("the program takes over the camera's NUC timer and hands it back") {
|
|
{
|
|
Harness t;
|
|
t.cam.open();
|
|
t.sdk.push(29315);
|
|
REQUIRE(t.online());
|
|
CHECK(t.sdk.nuc_interval == 0); // the camera's own timer is off
|
|
t.cam.close();
|
|
CHECK(t.sdk.nuc_interval == 30); // and back as found
|
|
}
|
|
{
|
|
auto o = fastOptions();
|
|
o.nuc_interval_ms = 0; // leave it to the camera
|
|
Harness t(o);
|
|
t.cam.open();
|
|
t.sdk.push(29315);
|
|
REQUIRE(t.online());
|
|
t.cam.moving();
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
|
CHECK(t.sdk.nuc_interval == 30);
|
|
CHECK(t.sdk.nucs == 0);
|
|
}
|
|
}
|
|
|
|
TEST_CASE("a move runs a due NUC, and no more often than the interval") {
|
|
auto o = fastOptions();
|
|
o.nuc_interval_ms = 400;
|
|
Harness t(o);
|
|
t.cam.open();
|
|
t.cam.start();
|
|
t.sdk.push(29315);
|
|
REQUIRE(t.online());
|
|
CHECK(t.sdk.nucs == 0);
|
|
t.cam.moving(); // the first move after connecting runs one
|
|
CHECK(waitFor([&] { return t.sdk.nucs == 1; }, 1000));
|
|
CHECK(t.sdk.nuc_mode == kNucMode1Point);
|
|
t.cam.moving(); // not due yet
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(150));
|
|
CHECK(t.sdk.nucs == 1);
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(300));
|
|
t.cam.moving(); // due again
|
|
CHECK(waitFor([&] { return t.sdk.nucs == 2; }, 1000));
|
|
}
|
|
|
|
TEST_CASE("an overdue NUC runs without a move") {
|
|
auto o = fastOptions();
|
|
o.nuc_interval_ms = 100;
|
|
Harness t(o);
|
|
t.cam.open();
|
|
t.sdk.push(29315);
|
|
REQUIRE(t.online());
|
|
// Twice the interval with no move at all (manual mode, say).
|
|
CHECK(waitFor([&] { return t.sdk.nucs >= 1; }, 1500));
|
|
}
|
|
|
|
TEST_CASE("after a NUC only a frame from after it is taken") {
|
|
auto o = fastOptions();
|
|
o.nuc_interval_ms = 400;
|
|
Harness t(o);
|
|
t.cam.open();
|
|
t.cam.start();
|
|
t.sdk.push(29315);
|
|
REQUIRE(t.online());
|
|
CHECK(t.cam.frameReady());
|
|
t.sdk.frozen = true; // nothing new comes during and after the NUC
|
|
t.cam.moving();
|
|
REQUIRE(waitFor([&] { return t.sdk.nucs == 1; }, 1000));
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
|
CHECK_FALSE(t.cam.frameReady()); // the frame from before the NUC is stale
|
|
CHECK_FALSE(t.cam.trigger());
|
|
t.sdk.frozen = false;
|
|
t.sdk.push(29400);
|
|
CHECK(waitFor([&] { return t.cam.frameReady(); }, 1000));
|
|
CHECK(t.shot());
|
|
}
|
|
|
|
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);
|
|
}
|