ua-client 1.2.0

Native OPC UA browser/inspector GUI built on async-opcua and egui
Documentation
use tokio::runtime::Runtime;
use tokio::sync::mpsc;

use crate::engine::{Engine, FilePickTarget, FrontendCtx};
use crate::messages::UiUpdate;
use crate::types::AuthMode;

pub struct UaApp {
    engine: Engine,
    update_rx: mpsc::UnboundedReceiver<UiUpdate>,
}

#[derive(Clone)]
struct EguiCtx(egui::Context);

impl FrontendCtx for EguiCtx {
    fn request_repaint(&self) {
        self.0.request_repaint();
    }

    fn set_clipboard(&self, text: &str) {
        let s = text.to_owned();
        self.0.output_mut(|o| o.copied_text = s);
    }

    fn pick_file(
        &self,
        rt: &Runtime,
        update_tx: &mpsc::UnboundedSender<UiUpdate>,
        target: FilePickTarget,
        title: &str,
        default_dir: &str,
    ) {
        let tx = update_tx.clone();
        let ctx = self.clone();
        let title = title.to_owned();
        let default_dir = default_dir.to_owned();
        rt.spawn_blocking(move || {
            let mut dlg = rfd::FileDialog::new().set_title(&title);
            if let Some(parent) = std::path::Path::new(&default_dir).parent()
                && parent.exists()
            {
                dlg = dlg.set_directory(parent);
            }
            if let Some(path) = dlg.pick_file() {
                let s = path.to_string_lossy().into_owned();
                let update = match target {
                    FilePickTarget::CertPath => UiUpdate::CertPathPicked(s),
                    FilePickTarget::KeyPath => UiUpdate::KeyPathPicked(s),
                };
                let _ = tx.send(update);
            }
            let _ = tx.send(UiUpdate::FilePickerClosed);
            ctx.request_repaint();
        });
    }
}

const STORAGE_ENDPOINT_URL: &str = "endpoint_url";
const STORAGE_ENDPOINT_HISTORY: &str = "endpoint_history";
const STORAGE_AUTH_MODE: &str = "auth_mode";
const STORAGE_AUTH_USERNAME: &str = "auth_username";
const STORAGE_AUTH_CERT_PATH: &str = "auth_cert_path";
const STORAGE_AUTH_KEY_PATH: &str = "auth_key_path";
const STORAGE_LAST_SELECTIONS: &str = "last_selection_paths";

impl UaApp {
    pub fn new(
        rt: Runtime,
        log_rx: mpsc::UnboundedReceiver<UiUpdate>,
        storage: Option<&dyn eframe::Storage>,
    ) -> Self {
        let (mut engine, update_rx) = Engine::new(rt, log_rx);
        if let Some(s) = storage {
            if let Some(url) = eframe::get_value::<String>(s, STORAGE_ENDPOINT_URL) {
                engine.model.endpoint_url = url;
            }
            if let Some(hist) = eframe::get_value::<Vec<String>>(s, STORAGE_ENDPOINT_HISTORY) {
                engine.model.endpoint_history = hist;
            }
            if let Some(m) = eframe::get_value::<String>(s, STORAGE_AUTH_MODE) {
                engine.model.auth_mode = match m.as_str() {
                    "UserName" => AuthMode::UserName,
                    "Certificate" => AuthMode::Certificate,
                    _ => AuthMode::Anonymous,
                };
            }
            if let Some(s2) = eframe::get_value::<String>(s, STORAGE_AUTH_USERNAME) {
                engine.model.auth_username = s2;
            }
            if let Some(s2) = eframe::get_value::<String>(s, STORAGE_AUTH_CERT_PATH) {
                engine.model.auth_cert_path = s2;
            }
            if let Some(s2) = eframe::get_value::<String>(s, STORAGE_AUTH_KEY_PATH) {
                engine.model.auth_key_path = s2;
            }
            if let Some(stored) = eframe::get_value::<std::collections::HashMap<String, Vec<String>>>(
                s,
                STORAGE_LAST_SELECTIONS,
            ) {
                use std::str::FromStr;
                for (url, ids) in stored {
                    let path: Vec<opcua::types::NodeId> = ids
                        .iter()
                        .filter_map(|s| opcua::types::NodeId::from_str(s).ok())
                        .collect();
                    if !path.is_empty() {
                        engine.model.last_selection_paths.insert(url, path);
                    }
                }
            }
        }
        Self { engine, update_rx }
    }
}

impl eframe::App for UaApp {
    fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
        let c = EguiCtx(ctx.clone());
        while let Ok(update) = self.update_rx.try_recv() {
            self.engine.apply_update(&c, update);
        }
        let mut actions = Vec::new();
        super::ui::draw(&self.engine.model, ctx, &mut actions);
        for action in actions {
            self.engine.dispatch(&c, action);
        }
    }

    fn save(&mut self, storage: &mut dyn eframe::Storage) {
        eframe::set_value(storage, STORAGE_ENDPOINT_URL, &self.engine.model.endpoint_url);
        eframe::set_value(
            storage,
            STORAGE_ENDPOINT_HISTORY,
            &self.engine.model.endpoint_history,
        );
        let auth_mode_str = match self.engine.model.auth_mode {
            AuthMode::Anonymous => "Anonymous",
            AuthMode::UserName => "UserName",
            AuthMode::Certificate => "Certificate",
        };
        eframe::set_value(storage, STORAGE_AUTH_MODE, &auth_mode_str.to_string());
        eframe::set_value(
            storage,
            STORAGE_AUTH_USERNAME,
            &self.engine.model.auth_username,
        );
        eframe::set_value(
            storage,
            STORAGE_AUTH_CERT_PATH,
            &self.engine.model.auth_cert_path,
        );
        eframe::set_value(
            storage,
            STORAGE_AUTH_KEY_PATH,
            &self.engine.model.auth_key_path,
        );
        let paths: std::collections::HashMap<String, Vec<String>> = self
            .engine
            .model
            .last_selection_paths
            .iter()
            .map(|(url, path)| (url.clone(), path.iter().map(|n| n.to_string()).collect()))
            .collect();
        eframe::set_value(storage, STORAGE_LAST_SELECTIONS, &paths);
    }
}