nebulus 0.1.29

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

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

const ALL_CHANNELS: [u8; 42] = [
    1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 36, 40, 44, 48, 52, 56, 60, 64, 100, 104, 108,
    112, 116, 120, 124, 128, 132, 136, 140, 144, 149, 153, 157, 161, 165, 169, 173, 177,
];

pub(crate) fn dialog(app: &mut NebulusApp, context: &egui::Context) {
    if !app.show_channel_scanner {
        return;
    }
    let mut open = true;
    let scanning = app.state == ReceiverState::Scanning;
    let idle = matches!(app.state, ReceiverState::Idle | ReceiverState::Failed);
    egui::Window::new("Channel scanner")
        .id(egui::Id::new("channel-scanner-window"))
        .open(&mut open)
        .resizable(true)
        .default_size([660.0, 540.0])
        .show(context, |ui| {
            ui.label(
                egui::RichText::new(
                    "Surveys RF traffic while RX is stopped. WFB activity and stronger RSSI help locate an active VTX.",
                )
                .small()
                .color(ui.visuals().weak_text_color()),
            );
            ui.add_space(8.0);
            ui.add_enabled_ui(idle, |ui| {
                ui.horizontal_wrapped(|ui| {
                    if ui.button("Common FPV").clicked() {
                        app.scan_channels = vec![
                            36, 40, 44, 48, 100, 104, 108, 112, 116, 120, 124, 128, 132,
                            136, 140, 144, 149, 153, 157, 161, 165, 169, 173, 177,
                        ];
                    }
                    if ui.button("2.4 GHz").clicked() {
                        app.scan_channels = (1..=14).collect();
                    }
                    if ui.button("Clear").clicked() {
                        app.scan_channels.clear();
                    }
                });
                egui::ScrollArea::horizontal()
                    .id_salt("scanner-channel-list")
                    .show(ui, |ui| {
                        ui.horizontal(|ui| {
                            for channel in ALL_CHANNELS {
                                let mut selected = app.scan_channels.contains(&channel);
                                if ui.toggle_value(&mut selected, channel.to_string()).changed() {
                                    if selected {
                                        app.scan_channels.push(channel);
                                        app.scan_channels.sort_unstable();
                                        app.scan_channels.dedup();
                                    } else {
                                        app.scan_channels.retain(|value| *value != channel);
                                    }
                                }
                            }
                        });
                    });
                ui.horizontal(|ui| {
                    ui.label("Dwell per channel");
                    ui.add(
                        egui::Slider::new(&mut app.scan_dwell_ms, 75..=1_000)
                            .suffix(" ms")
                            .logarithmic(true),
                    );
                });
            });
            ui.horizontal(|ui| {
                if scanning {
                    if ui.button("Stop scan").clicked() {
                        app.stop_receiver();
                    }
                } else if ui
                    .add_enabled(idle, egui::Button::new("Start survey"))
                    .clicked()
                {
                    app.start_channel_scan(context);
                }
                if let Some((done, total)) = app.scan_progress {
                    let fraction = if total == 0 {
                        0.0
                    } else {
                        done as f32 / total as f32
                    };
                    ui.add(
                        egui::ProgressBar::new(fraction)
                            .desired_width(220.0)
                            .text(format!("{done}/{total}")),
                    );
                }
            });
            if let Some(error) = &app.scan_error {
                ui.colored_label(ui.visuals().error_fg_color, error);
            }
            ui.separator();
            if app.scan_results.is_empty() {
                ui.label(
                    egui::RichText::new("No survey results yet")
                        .color(ui.visuals().weak_text_color()),
                );
            } else {
                egui::ScrollArea::vertical()
                    .id_salt("channel-scan-results")
                    .show(ui, |ui| {
                        egui::Grid::new("channel-scan-grid")
                            .num_columns(8)
                            .striped(true)
                            .spacing([14.0, 7.0])
                            .show(ui, |ui| {
                                for heading in [
                                    "Channel", "MHz", "Packets", "WFB", "RSSI A", "RSSI B",
                                    "Traffic", "",
                                ] {
                                    ui.strong(heading);
                                }
                                ui.end_row();
                                let results = app.scan_results.clone();
                                for result in results {
                                    ui.monospace(result.channel.to_string());
                                    ui.monospace(frequency_mhz(result.channel).to_string());
                                    ui.monospace(result.packets.to_string());
                                    ui.monospace(result.wfb_frames.to_string());
                                    ui.monospace(rssi_label(result.average_rssi_dbm[0]));
                                    ui.monospace(rssi_label(result.average_rssi_dbm[1]));
                                    let mbps = if result.dwell_ms == 0 {
                                        0.0
                                    } else {
                                        result.bytes as f64 * 8.0 / result.dwell_ms as f64 / 1_000.0
                                    };
                                    ui.monospace(format!("{mbps:.2} Mbps"));
                                    if ui.small_button("Use").clicked() {
                                        app.use_scanned_channel(result.channel);
                                    }
                                    ui.end_row();
                                }
                            });
                    });
            }
        });
    app.show_channel_scanner &= open;
}

fn frequency_mhz(channel: u8) -> u16 {
    match channel {
        14 => 2_484,
        1..=13 => 2_407 + u16::from(channel) * 5,
        _ => 5_000 + u16::from(channel) * 5,
    }
}

fn rssi_label(value: i32) -> String {
    if value == 0 {
        "--".to_owned()
    } else {
        format!("{value} dBm")
    }
}