nebulus 0.1.30

Low-latency native OpenIPC FPV ground station built with egui
use eframe::egui;

use crate::{app::NebulusApp, model::ReceiverState};

pub(crate) fn health(app: &NebulusApp, ui: &mut egui::Ui) {
    let counters = app.diagnostics.counters;
    match app
        .receiver_info
        .as_ref()
        .map(|receiver| receiver.transport)
    {
        Some(crate::runtime::ReceiverTransport::UdpRtp) => {
            health_row(ui, "UDP listener initialized", true);
            health_row(ui, "UDP datagrams arriving", app.metrics.usb_transfers > 0);
        }
        Some(crate::runtime::ReceiverTransport::Synthetic) => {
            health_row(ui, "Synthetic RTP source initialized", true);
            health_row(
                ui,
                "Synthetic packets arriving",
                app.metrics.usb_transfers > 0,
            );
        }
        Some(crate::runtime::ReceiverTransport::Usb) | None => {
            health_row(ui, "USB adapter initialized", app.receiver_info.is_some());
            health_row(ui, "USB transfers arriving", app.metrics.usb_transfers > 0);
            health_row(ui, "802.11 packets parsed", app.metrics.wifi_packets > 0);
            health_row(ui, "Frames accepted", counters.accepted_packets > 0);
            health_row(ui, "WFB payload recovered", counters.wfb_payloads > 0);
        }
    }
    health_row(ui, "RTP packets arriving", app.metrics.rtp_packets > 0);
    health_row(ui, "Codec configuration ready", codec_config_ready(app));
    health_row(
        ui,
        "Encoded frames extracted",
        app.metrics.encoded_frames > 0,
    );
    health_row(
        ui,
        "Platform decoder active",
        app.metrics.decoded_frames > 0,
    );
    health_row(
        ui,
        "Audio route healthy",
        !app.audio.enabled
            || (app.audio.supported && app.audio.decoded_frames > 0 && app.audio.errors == 0),
    );
    health_row(
        ui,
        "VPN bridge healthy",
        !app.settings.vpn_enabled || (app.vpn.active && app.vpn.errors == 0),
    );

    ui.add_space(10.0);
    ui.heading("Packet filtering");
    value_grid(ui, "health-counters", |ui| {
        row(ui, "Accepted", counters.accepted_packets);
        row(ui, "Dropped", counters.dropped_packets);
        row(ui, "CRC drops", counters.crc_dropped);
        row(ui, "ICV drops", counters.icv_dropped);
        row(ui, "Reports dropped", counters.report_dropped);
        row(ui, "Ignored frames", counters.ignored_frames);
        row(ui, "WFB sessions", counters.sessions);
        row(ui, "Route errors", counters.route_errors);
    });
    if !app.adapter_metrics.is_empty() {
        ui.add_space(10.0);
        ui.heading("Receive adapters");
        if app.adapter_metrics.len() > 1 {
            value_grid(ui, "diversity-summary", |ui| {
                row(ui, "Unique packets", app.diagnostics.diversity.accepted);
                row(ui, "Duplicate copies", app.diagnostics.diversity.duplicates);
                row(
                    ui,
                    "Dedup cache",
                    app.diagnostics.diversity.cached_packets as u64,
                );
            });
        }
        for adapter in &app.adapter_metrics {
            ui.add_space(6.0);
            ui.strong(format!(
                "Radio {} · {}",
                adapter.source_id + 1,
                adapter.label
            ));
            value_grid(ui, ("diversity-adapter", adapter.source_id), |ui| {
                text_row(ui, "Device", &adapter.device_id);
                text_row(
                    ui,
                    "State",
                    if adapter.online { "Online" } else { "Offline" },
                );
                row(ui, "USB transfers", adapter.transfers);
                row(ui, "USB bytes", adapter.transfer_bytes);
                if app.adapter_metrics.len() > 1 {
                    row(ui, "First-copy wins", adapter.accepted);
                    row(ui, "Duplicate copies", adapter.duplicates);
                }
                row(ui, "USB errors", adapter.usb_errors);
                row(ui, "Queue drops", adapter.queue_drops);
                text_row(
                    ui,
                    "RSSI",
                    &format!("{}/{} dBm", adapter.rssi[0], adapter.rssi[1]),
                );
                text_row(
                    ui,
                    "SNR",
                    &format!("{}/{} dB", adapter.snr[0], adapter.snr[1]),
                );
            });
        }
    }
}

pub(crate) fn rtp(app: &NebulusApp, ui: &mut egui::Ui) {
    let status = app.diagnostics.rtp;
    let reorder = app.diagnostics.reorder;
    value_grid(ui, "rtp-diagnostics", |ui| {
        text_row(ui, "Codec", &format!("{:?}", status.last_codec));
        text_row(
            ui,
            "H.264 config",
            &format!(
                "SPS {} / PPS {}",
                yes_no(status.codec_config.h264_sps),
                yes_no(status.codec_config.h264_pps),
            ),
        );
        text_row(
            ui,
            "H.265 config",
            &format!(
                "VPS {} / SPS {} / PPS {}",
                yes_no(status.codec_config.h265_vps),
                yes_no(status.codec_config.h265_sps),
                yes_no(status.codec_config.h265_pps),
            ),
        );
        option_row(ui, "Payload type", status.last_payload_type);
        option_row(ui, "NAL type", status.last_nal_type);
        option_row(ui, "Sequence", status.last_sequence_number);
        option_row(ui, "RTP timestamp", status.last_timestamp);
        row(ui, "Packets", status.packets);
        row(ui, "Frames emitted", status.frames_emitted);
        row(ui, "Config wait drops", status.config_wait_drops);
        row(
            ui,
            "Keyframes with config",
            status.keyframes_with_prepended_config,
        );
        row(
            ui,
            "Parameter sets prepended",
            status.parameter_sets_prepended,
        );
        row(ui, "Fragment gaps", status.fragment_sequence_gaps);
        row(ui, "Fragment overflows", status.fragment_overflows);
        row(ui, "Malformed", status.malformed_packets);
        row(ui, "Unsupported payloads", status.unsupported_payloads);
        row(ui, "Reorder buffered", reorder.buffered_packets);
        row(ui, "Reordered", reorder.reordered_packets);
        row(ui, "Late packets", reorder.late_packets);
        row(ui, "Forced flushes", reorder.forced_flushes);
    });
}

pub(crate) fn latency(app: &NebulusApp, ui: &mut egui::Ui) {
    if app.diagnostics.stages.is_empty() {
        ui.label(
            egui::RichText::new("No latency samples yet").color(ui.visuals().weak_text_color()),
        );
        return;
    }

    if ui.available_width() < 700.0 {
        latency_cards(app, ui);
        return;
    }

    egui::Grid::new("latency-stages")
        .num_columns(6)
        .striped(true)
        .spacing([10.0, 7.0])
        .show(ui, |ui| {
            for heading in ["Stage", "Last (ms)", "Average", "P95", "Maximum", "Samples"] {
                ui.strong(heading);
            }
            ui.end_row();
            for (name, values) in &app.diagnostics.stages {
                let summary = values.summary();
                ui.label(*name);
                ui.monospace(format!("{:.2}", summary.last));
                ui.monospace(format!("{:.2}", summary.average));
                ui.monospace(format!("{:.2}", summary.p95));
                ui.monospace(format!("{:.2}", summary.maximum));
                ui.monospace(summary.samples.to_string());
                ui.end_row();
            }
        });
}

fn latency_cards(app: &NebulusApp, ui: &mut egui::Ui) {
    for (name, values) in &app.diagnostics.stages {
        let summary = values.summary();
        egui::Frame::group(ui.style())
            .inner_margin(egui::Margin::symmetric(10, 8))
            .show(ui, |ui| {
                ui.horizontal(|ui| {
                    ui.strong(*name);
                    ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
                        ui.label(
                            egui::RichText::new(format!("{} samples", summary.samples))
                                .small()
                                .color(ui.visuals().weak_text_color()),
                        );
                    });
                });
                ui.add_space(5.0);
                ui.columns(4, |columns| {
                    latency_stat(&mut columns[0], "Last", summary.last);
                    latency_stat(&mut columns[1], "Average", summary.average);
                    latency_stat(&mut columns[2], "P95", summary.p95);
                    latency_stat(&mut columns[3], "Maximum", summary.maximum);
                });
            });
        ui.add_space(6.0);
    }
}

fn latency_stat(ui: &mut egui::Ui, label: &str, value_ms: f64) {
    ui.label(
        egui::RichText::new(label)
            .small()
            .color(ui.visuals().weak_text_color()),
    );
    ui.monospace(format!("{value_ms:.2} ms"));
}

pub(crate) fn system(app: &mut NebulusApp, ui: &mut egui::Ui) {
    ui.horizontal_wrapped(|ui| {
        if ui.button("Export support bundle").clicked() {
            app.export_support_bundle();
        }
        ui.label(
            egui::RichText::new("Includes diagnostics and logs; excludes the WFB key")
                .small()
                .color(ui.visuals().weak_text_color()),
        );
    });
    ui.add_space(10.0);

    let environment = &app.environment;
    let maximum_fps = if environment.maximum_observed_fps > 0.0 {
        format!("{:.1} FPS observed", environment.maximum_observed_fps)
    } else {
        "Not reported; waiting for stream".to_owned()
    };
    value_grid(ui, "environment-details", |ui| {
        text_row(ui, "Platform", &environment.platform);
        text_row(ui, "Architecture", &environment.architecture);
        text_row(ui, "Runtime", &environment.runtime);
        text_row(ui, "Renderer", &environment.renderer);
        text_row(ui, "Logical processors", &environment.logical_processors);
        text_row(ui, "User agent", &environment.user_agent);
        text_row(ui, "Media backend", &environment.decoder_backend);
        text_row(ui, "H.264", &environment.h264);
        text_row(ui, "H.265", &environment.h265);
        text_row(
            ui,
            "Native/GPU surfaces",
            if environment.native_surfaces {
                "Yes"
            } else {
                "No"
            },
        );
        text_row(
            ui,
            "Maximum resolution",
            &environment
                .maximum_observed_resolution
                .map(|[width, height]| format!("{width} x {height} observed"))
                .unwrap_or_else(|| "Not reported; waiting for stream".to_owned()),
        );
        text_row(ui, "Maximum frame rate", &maximum_fps);
        text_row(
            ui,
            "Receiver state",
            match app.state {
                ReceiverState::Idle => "Idle",
                ReceiverState::Connecting => "Connecting",
                ReceiverState::Ready => "Ready",
                ReceiverState::Receiving => "Receiving",
                ReceiverState::Scanning => "Scanning",
                ReceiverState::Stopping => "Stopping",
                ReceiverState::Failed => "Failed",
            },
        );
        text_row(
            ui,
            "USB API",
            if cfg!(target_arch = "wasm32") {
                "nusb WebUSB"
            } else if cfg!(target_os = "android") {
                "Android UsbManager + nusb fd"
            } else {
                "nusb native"
            },
        );
        text_row(
            ui,
            "VPN/TUN",
            if cfg!(target_arch = "wasm32") {
                "Unavailable in browser"
            } else {
                "Native TUN supported"
            },
        );
    });
    ui.add_space(8.0);
    ui.label(
        egui::RichText::new(
            "Platform decoders generally do not expose a reliable global maximum resolution or frame rate. Nebulus reports the highest stream values observed in this session.",
        )
        .small()
        .color(ui.visuals().weak_text_color()),
    );
}

fn codec_config_ready(app: &NebulusApp) -> bool {
    app.diagnostics
        .rtp
        .last_codec
        .is_some_and(|codec| app.diagnostics.rtp.codec_config.is_complete_for(codec))
}

fn health_row(ui: &mut egui::Ui, label: &str, healthy: bool) {
    ui.horizontal(|ui| {
        let (rect, response) = ui.allocate_exact_size(egui::vec2(14.0, 14.0), egui::Sense::hover());
        let color = if healthy {
            egui::Color32::from_rgb(61, 214, 154)
        } else {
            ui.visuals().weak_text_color()
        };
        if healthy {
            ui.painter().circle_filled(rect.center(), 4.0, color);
        } else {
            ui.painter()
                .circle_stroke(rect.center(), 4.5, egui::Stroke::new(1.5, color));
        }
        response.on_hover_text(if healthy { "Healthy" } else { "Waiting" });
        ui.label(label);
    });
}

fn value_grid(
    ui: &mut egui::Ui,
    id: impl std::hash::Hash + std::fmt::Debug,
    add: impl FnOnce(&mut egui::Ui),
) {
    let max_column_width = ((ui.available_width() - 20.0) * 0.5).max(80.0);
    egui::Grid::new(id)
        .num_columns(2)
        .striped(true)
        .max_col_width(max_column_width)
        .spacing([20.0, 7.0])
        .show(ui, add);
}

fn yes_no(value: bool) -> &'static str {
    if value {
        "yes"
    } else {
        "no"
    }
}

fn row(ui: &mut egui::Ui, label: &str, value: impl std::fmt::Display) {
    text_row(ui, label, &value.to_string());
}

fn option_row(ui: &mut egui::Ui, label: &str, value: Option<impl std::fmt::Display>) {
    text_row(
        ui,
        label,
        &value.map_or_else(|| "--".to_owned(), |value| value.to_string()),
    );
}

fn text_row(ui: &mut egui::Ui, label: &str, value: &str) {
    ui.label(egui::RichText::new(label).color(ui.visuals().weak_text_color()));
    ui.monospace(value);
    ui.end_row();
}