fwt_software/include/fgc/mock/MockCameraSource.h

174 lines
7.2 KiB
C++

#pragma once
#include "fgc/ICameraSource.h"
#include "fgc/Logger.h"
#include <algorithm>
#include <chrono>
#include <map>
#include <string>
#include <thread>
namespace fgc {
// Synthetic camera for development without hardware. On trigger() it generates
// a small RGB gradient frame (varying with time so successive captures differ)
// and delivers it to the frame callback. setProfile() only records the profile
// name - there is no sensor to configure - which is enough to exercise profile
// switching end to end with --mock-camera.
class MockCameraSource : public ICameraSource {
public:
explicit MockCameraSource(int count = 1, uint32_t width = 640, uint32_t height = 480)
: count_(count), width_(width), height_(height) {}
void open() override { LOG_INFO << "[mock] camera opened (" << count_ << ")"; }
void close() override {}
void start() override { started_ = true; LOG_INFO << "[mock] camera acquisition started"; }
void stop() override { started_ = false; }
bool trigger() override {
if (!callback_) return false;
LOG_TRACE_CAT(LogCat::Camera) << "TX trigger cam0 (mock)";
Frame f = synth();
LOG_TRACE_CAT(LogCat::Camera)
<< "RX frame cam0 " << width_ << 'x' << height_ << ' ' << f.data.size() << "B (mock)";
++frames_;
callback_(f);
return true;
}
void setFrameCallback(FrameCallback cb) override { callback_ = std::move(cb); }
// No auto-exposure to wait for: settled as soon as the gimbal is at rest, so
// --mock-camera runs exercise the automatic settle wait.
ExposureState exposureState() override { return ExposureState::Settled; }
int cameraCount() const override { return count_; }
bool setProfile(int camera_index, const CameraProfile& profile) override {
profile_ = profile.name;
LOG_INFO << "[mock] camera " << camera_index << " profile -> " << profile_;
return true;
}
// `test camera`. The mock behaves like the real one: minimum exposure and gain
// drive it to black, maximum gain brightens it, so the self-test passes against
// it - and a fault can be switched on to prove the test fails when it should.
enum class Fault { None, NoFrames, IgnoresSettings };
void setFault(Fault f) { fault_ = f; }
bool grabFrame(int camera_index, Frame& out, int timeout_ms) override {
if (!started_ || camera_index < 0 || camera_index >= count_) return false;
if (fault_ == Fault::NoFrames) {
std::this_thread::sleep_for(std::chrono::milliseconds(std::min(timeout_ms, 20)));
return false;
}
out = synth();
out.cam_id = camera_index;
++frames_;
return true;
}
bool setFeature(int camera_index, const std::string& name, const std::string& value) override {
if (camera_index < 0 || camera_index >= count_) return false;
features_[name] = value;
return true;
}
bool featureRange(int camera_index, const std::string& name, double& min,
double& max) override {
if (camera_index < 0 || camera_index >= count_) return false;
if (name == "ExposureTime") { min = 66.357; max = 9999971.0; return true; }
if (name == "Gain") { min = 0.0; max = 48.0; return true; }
if (name == "BlackLevel") { min = 0.0; max = 4095.0; return true; }
return false;
}
bool restoreProfile(int camera_index) override {
if (camera_index < 0 || camera_index >= count_) return false;
features_.clear();
return true;
}
bool resetCamera(int camera_index) override {
if (camera_index < 0 || camera_index >= count_) return false;
LOG_INFO << "[mock] camera " << camera_index << " reset";
features_.clear();
fault_ = Fault::None; // a reset is what clears a stuck camera
return true;
}
// Synthetic device info so the expanded camera view renders (and is testable)
// without hardware. Non-hardware fields (temperature, exposure...) stay at 0.
std::vector<CameraDeviceInfo> deviceInfo() override {
std::vector<CameraDeviceInfo> out;
for (int i = 0; i < count_; ++i) {
CameraDeviceInfo d;
d.id = "MOCK" + std::to_string(i);
d.model = "MockCamera";
d.serial = "SIM-0000";
d.streaming = started_;
d.width = width_;
d.height = height_;
d.payload_bytes = static_cast<long long>(width_) * height_ * 3;
d.actual_fps = 1.0;
d.frames_delivered = (i == 0) ? frames_ : 0; // only cam0 produces frames
out.push_back(std::move(d));
}
return out;
}
private:
// A mid-grey gradient frame. The pattern varies with time so successive
// captures differ.
Frame synth() const {
Frame f;
f.width = width_;
f.height = height_;
f.channels = 3;
f.cam_id = 0;
f.timestamp_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch())
.count();
f.data.resize(static_cast<size_t>(width_) * height_ * 3);
// The scene's mid grey, unless the self-test has forced an extreme.
double level = 110.0;
auto num = [&](const char* k, double dflt) {
auto it = features_.find(k);
return it == features_.end() ? dflt : std::stod(it->second);
};
if (fault_ != Fault::IgnoresSettings && features_.count("ExposureTime")) {
if (num("ExposureTime", 1e9) < 100.0 && num("Gain", 99.0) <= 0.0)
level = -100.0; // minimum exposure and gain: black
else if (num("Gain", 0.0) >= 48.0)
level = 180.0; // maximum gain: visibly brighter
}
const uint8_t phase = static_cast<uint8_t>(f.timestamp_ms);
for (uint32_t y = 0; y < height_; ++y) {
for (uint32_t x = 0; x < width_; ++x) {
size_t i = (static_cast<size_t>(y) * width_ + x) * 3;
// Zero-mean texture around `level`. The cast matters:
// (x+y+phase)%3 is unsigned, so subtracting 1 from the zero case
// would wrap to UINT_MAX and clip a third of the frame to white.
const double tex = 20.0 * (static_cast<int>((x + y + phase) % 3) - 1);
auto clamp8 = [](double v) {
return static_cast<uint8_t>(v < 0 ? 0 : (v > 255 ? 255 : v));
};
f.data[i + 0] = clamp8(level + tex);
f.data[i + 1] = clamp8(level + tex * 0.8);
f.data[i + 2] = clamp8(level + tex * 0.6);
}
}
return f;
}
int count_;
uint32_t width_;
uint32_t height_;
bool started_ = false;
long long frames_ = 0;
std::string profile_;
FrameCallback callback_;
Fault fault_ = Fault::None;
std::map<std::string, std::string> features_; // what setFeature() has set
};
} // namespace fgc