use eframe::egui;
use crate::{app::NebulusApp, model::ReceiverState};
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
enum View {
#[default]
Health,
Rtp,
Latency,
Environment,
}
pub(crate) fn show(app: &mut NebulusApp, ui: &mut egui::Ui) {
ui.horizontal(|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(5.0);
let id = ui.make_persistent_id("diagnostics-view");
let mut view = ui.data(|data| data.get_temp::<View>(id).unwrap_or_default());
ui.horizontal_wrapped(|ui| {
for (candidate, label) in [
(View::Health, "Pipeline health"),
(View::Rtp, "RTP"),
(View::Latency, "Stage latency"),
(View::Environment, "Environment"),
] {
if ui.selectable_label(view == candidate, label).clicked() {
view = candidate;
}
}
});
ui.data_mut(|data| data.insert_temp(id, view));
ui.separator();
match view {
View::Health => health(app, ui),
View::Rtp => rtp(app, ui),
View::Latency => latency(app, ui),
View::Environment => environment(app, ui),
}
}
fn health(app: &NebulusApp, ui: &mut egui::Ui) {
let counters = app.diagnostics.counters;
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]),
);
});
}
}
}
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);
});
}
fn latency(app: &NebulusApp, ui: &mut egui::Ui) {
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"));
}
fn environment(app: &NebulusApp, ui: &mut egui::Ui) {
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();
}