#pragma once #include "fgc/ICameraSource.h" #include "fgc/Logger.h" #include #include #include #include #include #include namespace fgc { // Synthetic camera for development without hardware. On trigger() it generates // a small gradient frame (varying with time so successive captures differ) and // delivers it to the frame callback - as RGB8 by default, as the raw mosaic // (BayerRG8, BayerRG12p) a raw-capture profile would deliver, or as a thermal // camera's counts (Mono16: a 15..25 degC gradient with one 300 degC spot that // moves with time, profile 0). 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, PixelLayout layout = PixelLayout::RGB8) : count_(count), width_(width), height_(height), layout_(layout) {} 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); } // A free-running stream whose auto-exposure has settled: every call to // framesSince() "delivers" one more frame at the camera's target brightness // (50% of full scale), so the keep rule settles after a few scheduler ticks. uint64_t frameSeq() override { return stream_seq_; } std::vector framesSince(uint64_t after) override { ++stream_seq_; std::vector out; for (uint64_t s = after + 1; s <= stream_seq_; ++s) out.push_back(FrameStats{s, 127.5, 10000.0, 6.0}); return out; } bool readFeature(int camera_index, const std::string& name, double& value) override { if (camera_index < 0 || camera_index >= count_) return false; if (layout_ == PixelLayout::Mono16) { // a thermal camera, like TarisCameraSource if (name == "CalibProfile") { value = 0.0; return true; } if (name == "KelvinPerCount") { value = 0.01; return true; } return false; } if (name == "IntensityControllerTarget") { value = 50.0; return true; } if (name == "IntensityControllerTolerance") { value = 5.0; return true; } if (name == "ExposureAutoMin") { value = 82.0; return true; } if (name == "ExposureAutoMax") { value = 20000.0; return true; } if (name == "GainAutoMin") { value = 0.0; return true; } if (name == "GainAutoMax") { value = 24.0; return true; } return false; } 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 (layout_ == PixelLayout::Mono16) return false; // a thermal camera: no exposure 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 deviceInfo() override { std::vector 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(layoutFrameBytes(layout_, width_, height_)); 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 { if (layout_ == PixelLayout::Mono16) return thermal(); Frame f; f.width = width_; f.height = height_; f.channels = 3; f.cam_id = 0; f.timestamp_ms = std::chrono::duration_cast( std::chrono::system_clock::now().time_since_epoch()) .count(); f.data.resize(static_cast(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(f.timestamp_ms); for (uint32_t y = 0; y < height_; ++y) { for (uint32_t x = 0; x < width_; ++x) { size_t i = (static_cast(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((x + y + phase) % 3) - 1); auto clamp8 = [](double v) { return static_cast(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); } } f.layout = PixelLayout::RGB8; if (layoutIsBayer(layout_)) f = mosaic(f); f.brightness = sampleBrightness(f.data.data(), f.data.size(), f.layout, f.width, f.height); return f; } // A thermal scene in counts at 0.01 K per count. Frame thermal() const { Frame f; f.width = width_; f.height = height_; f.channels = 1; f.layout = PixelLayout::Mono16; f.cam_id = 0; f.timestamp_ms = std::chrono::duration_cast( std::chrono::system_clock::now().time_since_epoch()) .count(); std::vector px(static_cast(width_) * height_); for (uint32_t y = 0; y < height_; ++y) for (uint32_t x = 0; x < width_; ++x) // 288.15 K at the top .. 298.15 K px[static_cast(y) * width_ + x] = static_cast(28815 + 1000 * y / std::max(1, height_ - 1)); const uint32_t hx = static_cast(f.timestamp_ms / 100) % width_; px[static_cast(height_ / 2) * width_ + hx] = 57315; // 300 degC f.data.resize(px.size() * 2); std::memcpy(f.data.data(), px.data(), f.data.size()); f.brightness = sampleBrightness(f.data.data(), f.data.size(), f.layout, f.width, f.height); f.thermal = ThermalFrameInfo{}; return f; } // The RGB frame as the sensor would see it: each pixel keeps only the colour // of its filter (BayerRG: R G / G B), 8-bit or packed 12-bit. Frame mosaic(const Frame& rgb) const { Frame f = rgb; f.layout = layout_; f.channels = 1; const size_t n = static_cast(width_) * height_; std::vector m(n); for (uint32_t y = 0; y < height_; ++y) for (uint32_t x = 0; x < width_; ++x) { const int c = (y % 2 == 0) ? (x % 2 == 0 ? 0 : 1) : (x % 2 == 0 ? 1 : 2); m[static_cast(y) * width_ + x] = rgb.data[(static_cast(y) * width_ + x) * 3 + c]; } f.data.assign(layoutFrameBytes(layout_, width_, height_), 0); if (layout_ == PixelLayout::BayerRG8) { for (size_t i = 0; i < n; ++i) f.data[i] = static_cast(m[i]); } else { // BayerRG12p: the 8-bit value << 4, packed two pixels in three bytes for (size_t i = 0, o = 0; i < n; i += 2, o += 3) { const uint16_t p0 = static_cast(m[i] << 4); const uint16_t p1 = i + 1 < n ? static_cast(m[i + 1] << 4) : 0; f.data[o] = static_cast(p0 & 0xFF); f.data[o + 1] = static_cast(((p0 >> 8) & 0x0F) | ((p1 & 0x0F) << 4)); if (o + 2 < f.data.size()) f.data[o + 2] = static_cast(p1 >> 4); } } return f; } int count_; uint32_t width_; uint32_t height_; PixelLayout layout_; uint64_t stream_seq_ = 1; bool started_ = false; long long frames_ = 0; std::string profile_; FrameCallback callback_; Fault fault_ = Fault::None; std::map features_; // what setFeature() has set }; } // namespace fgc