Program Listing for File session_health.hpp#

Return to documentation for file (src/session/session_health.hpp)

#pragma once
#include <QString>
#include <QVector>
#include <optional>

#include "calibration/rms_quality.hpp"

namespace mosaic {

// One camera's already-gathered raw numbers for a just-finished recording —
// plain data, with no dependency on VideoManager/VideoGrabber, so
// build_session_health_report() is directly unit-testable with hand-built
// fixtures. The caller (MainWindow::show_session_health()) fills this in from
// VideoManager::last_recording_snapshot() plus last_recording_action_ticks()
// and a SyncManifest lookup — deliberately the post-recording snapshots, not
// the live camera_stats()/action_ticks_fired() reads, which the preview
// restart following recording_stopped has already zeroed by then.
struct CameraHealthInput {
    int index = 0;
    QString name;

    // False for a camera that was configured but never actually opened for
    // this recording — a duplicate/ambiguous serial, a failed open(), or a
    // dead cable/NIC link (a real, recurring failure mode on this rig). Such a
    // camera has no counters at all, so every numeric field below stays at its
    // default and must NOT be read as "captured nothing, cleanly" — hence the
    // explicit flag rather than inferring absence from a zero counter.
    bool participated = true;

    int64_t framesGrabbed    = 0;
    int64_t framesEncoded    = 0;
    int64_t framesDropped    = 0; // ring-buffer overflow
    int64_t incompleteFrames = 0; // GVSP packet loss
    double configuredFps     = 0.0;
    double achievableFps     = -1.0; // -1 = not yet measured

    // nullopt if this camera didn't use Action1 (GigE Vision Action
    // Command) triggering this session — see VideoGrabber::action_command_ready().
    std::optional<int64_t> actionTicksFired;

    // nullopt if SyncManifest::generate() produced no entry for this camera
    // (e.g. it captured zero frames, or sync data wasn't generated at all).
    std::optional<double> syncCoveragePct;
    std::optional<double> syncMeanDeltaMs;
    std::optional<double> syncMaxDeltaMs;
};

struct CameraHealthEntry {
    CameraHealthInput raw;

    // max(0, actionTicksFired - framesGrabbed). nullopt iff
    // raw.actionTicksFired is nullopt (Action1 triggering wasn't used).
    std::optional<int64_t> missedTriggerFrames;

    RmsQuality quality = RmsQuality::Excellent;
};

struct SessionHealthReport {
    QString sessionPath;
    QString sessionName;
    int64_t durationMs = 0;
    QVector<CameraHealthEntry> cameras;
    RmsQuality overallQuality = RmsQuality::Excellent; // worst tier across all cameras
    QString headline;                                  // e.g. "3/3 cameras clean"
};

// Buckets one camera's health from its raw counters:
//   - Poor if the camera never opened for this recording (!participated), and
//     Poor if it opened but captured zero frames. Both are checked first: such
//     a camera has no drop/incomplete/sync data to be "bad" in, and its
//     achievableFps stays at the "not measured" sentinel, so it would
//     otherwise fall through every check below and grade Excellent — the exact
//     inversion that made a disconnected camera outrank a healthy one.
//   - Poor if there's any ring-buffer drop or GVSP packet loss at all (both
//     mean lost data, not a fuzzy tradeoff), sync coverage is badly low
//     (<80%), or the missed-trigger gap exceeds the same threshold (5)
//     ActionCommandTicker's own log-warning already uses for "worth flagging".
//   - Acceptable for a borderline sync coverage (<95%) or a real-but-smaller
//     achievable-fps shortfall (<90% of configured).
//   - Good for a smaller still achievable-fps shortfall (<98% of configured).
//   - Excellent otherwise.
// Mirrors rppg_quality_for()'s "hard override for a structural problem,
// graded tiers otherwise" shape (src/analysis/realtime_metrics.cpp).
// Thresholds are documented, reasoned defaults, not derived from a
// calibration study — same honest caveat rppg_quality_for()'s own doc
// comment already carries for its own thresholds.
[[nodiscard]] RmsQuality camera_health_quality_for(const CameraHealthInput& raw,
                                                   std::optional<int64_t> missedTriggerFrames);

// Pure aggregation — see CameraHealthInput's own doc comment for why this
// takes plain data rather than live VideoManager/VideoGrabber objects.
[[nodiscard]] SessionHealthReport build_session_health_report(
    const QString& sessionPath, const QString& sessionName, int64_t durationMs,
    const QVector<CameraHealthInput>& cameras);

} // namespace mosaic