Skip to main content

ua_client/
model.rs

1use std::collections::{HashMap, HashSet, VecDeque};
2
3use opcua::types::{NodeId, ObjectId};
4
5use crate::types::{
6    AuthMode, EndpointInfo, LogLine, MethodCallOutcome, MethodSignature, NodeSummary, ReferenceRow,
7    SecurityMode, TreeChild,
8};
9
10#[derive(Debug, Clone)]
11pub enum MethodCallState {
12    Loading {
13        node: NodeId,
14    },
15    Failed {
16        node: NodeId,
17        error: String,
18    },
19    Inputs {
20        node: NodeId,
21        signature: MethodSignature,
22        edited: Vec<String>,
23        field_errors: Vec<Option<String>>,
24        call_error: Option<String>,
25    },
26    Calling {
27        node: NodeId,
28        signature: MethodSignature,
29        edited: Vec<String>,
30    },
31    Result {
32        node: NodeId,
33        signature: MethodSignature,
34        edited: Vec<String>,
35        outcome: MethodCallOutcome,
36    },
37}
38
39impl MethodCallState {
40    pub fn node(&self) -> &NodeId {
41        match self {
42            MethodCallState::Loading { node }
43            | MethodCallState::Failed { node, .. }
44            | MethodCallState::Inputs { node, .. }
45            | MethodCallState::Calling { node, .. }
46            | MethodCallState::Result { node, .. } => node,
47        }
48    }
49}
50
51const MAX_LOG_LINES: usize = 1000;
52const MAX_HISTORY: usize = 20;
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55pub enum ConnectionState {
56    Disconnected,
57    Connecting,
58    Connected,
59    Disconnecting,
60}
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub enum DetailTab {
64    Attributes,
65    Events,
66    DataChanges,
67    References,
68}
69
70#[derive(Debug, Default)]
71pub struct TreeModel {
72    pub children: HashMap<NodeId, Vec<TreeChild>>,
73    pub expanded: HashSet<NodeId>,
74    pub loading: HashSet<NodeId>,
75}
76
77impl TreeModel {
78    pub fn clear(&mut self) {
79        self.children.clear();
80        self.expanded.clear();
81        self.loading.clear();
82    }
83}
84
85pub struct AppModel {
86    pub endpoint_url: String,
87    pub endpoint_history: Vec<String>,
88    pub connection: ConnectionState,
89    pub root_node: NodeId,
90    pub tree: TreeModel,
91    pub selected: Option<NodeId>,
92    pub node_summary: Option<NodeSummary>,
93    pub active_tab: DetailTab,
94    pub references: Option<Vec<ReferenceRow>>,
95    pub references_loading: bool,
96    pub log: VecDeque<LogLine>,
97    pub selected_endpoint: Option<EndpointInfo>,
98    pub endpoints_loading: bool,
99    pub discovered_endpoints: Option<Vec<EndpointInfo>>,
100    pub endpoints_dialog_open: bool,
101    pub auth_mode: AuthMode,
102    pub auth_username: String,
103    pub auth_password: String,
104    pub auth_cert_path: String,
105    pub auth_key_path: String,
106    pub last_selection_paths: HashMap<String, Vec<NodeId>>,
107    pub last_connection_selections: HashMap<String, ConnectionPrefs>,
108    pub endpoint_mode_filter: SecurityMode,
109    pub file_picker_open: bool,
110    pub method_call: Option<MethodCallState>,
111}
112
113#[derive(Debug, Clone, Default)]
114pub struct ConnectionPrefs {
115    pub auth_mode: AuthMode,
116    pub security_mode: SecurityMode,
117    pub username: String,
118    pub cert_path: String,
119    pub key_path: String,
120}
121
122impl Default for AppModel {
123    fn default() -> Self {
124        Self {
125            endpoint_url: "opc.tcp://localhost:4855".to_string(),
126            endpoint_history: Vec::new(),
127            connection: ConnectionState::Disconnected,
128            root_node: NodeId::new(0, ObjectId::RootFolder as u32),
129            tree: TreeModel::default(),
130            selected: None,
131            node_summary: None,
132            active_tab: DetailTab::References,
133            references: None,
134            references_loading: false,
135            log: VecDeque::with_capacity(MAX_LOG_LINES),
136            selected_endpoint: None,
137            endpoints_loading: false,
138            discovered_endpoints: None,
139            endpoints_dialog_open: false,
140            auth_mode: AuthMode::Anonymous,
141            auth_username: String::new(),
142            auth_password: String::new(),
143            auth_cert_path: String::new(),
144            auth_key_path: String::new(),
145            last_selection_paths: HashMap::new(),
146            last_connection_selections: HashMap::new(),
147            endpoint_mode_filter: SecurityMode::None,
148            file_picker_open: false,
149            method_call: None,
150        }
151    }
152}
153
154impl AppModel {
155    pub fn push_log(&mut self, line: LogLine) {
156        if self.log.len() == MAX_LOG_LINES {
157            self.log.pop_front();
158        }
159        self.log.push_back(line);
160    }
161
162    pub fn reset_session_state(&mut self) {
163        self.tree.clear();
164        self.selected = None;
165        self.node_summary = None;
166        self.references = None;
167        self.references_loading = false;
168        self.method_call = None;
169    }
170
171    pub fn record_successful_connection(&mut self) {
172        let url = self.endpoint_url.trim().to_string();
173        if url.is_empty() {
174            return;
175        }
176        self.endpoint_history.retain(|u| u != &url);
177        self.endpoint_history.insert(0, url.clone());
178        self.endpoint_history.truncate(MAX_HISTORY);
179        self.last_connection_selections.insert(
180            url,
181            ConnectionPrefs {
182                auth_mode: self.auth_mode,
183                security_mode: self.endpoint_mode_filter,
184                username: self.auth_username.clone(),
185                cert_path: self.auth_cert_path.clone(),
186                key_path: self.auth_key_path.clone(),
187            },
188        );
189    }
190
191    pub fn apply_saved_connection_prefs(&mut self) {
192        let Some(prefs) = self.last_connection_selections.get(&self.endpoint_url).cloned() else {
193            return;
194        };
195        self.auth_mode = prefs.auth_mode;
196        self.endpoint_mode_filter = prefs.security_mode;
197        self.auth_username = prefs.username;
198        self.auth_cert_path = prefs.cert_path;
199        self.auth_key_path = prefs.key_path;
200    }
201}