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, SubscriptionRow, TreeChild, WriteTarget,
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
51#[derive(Debug, Clone)]
52pub enum AttributeEditState {
53    Loading {
54        node: NodeId,
55        attr_name: String,
56    },
57    Failed {
58        node: NodeId,
59        attr_name: String,
60        error: String,
61    },
62    Inputs {
63        node: NodeId,
64        attr_name: String,
65        target: WriteTarget,
66        edited: String,
67        field_error: Option<String>,
68        write_error: Option<String>,
69    },
70    Writing {
71        node: NodeId,
72        attr_name: String,
73        target: WriteTarget,
74        edited: String,
75    },
76}
77
78impl AttributeEditState {
79    pub fn node(&self) -> &NodeId {
80        match self {
81            AttributeEditState::Loading { node, .. }
82            | AttributeEditState::Failed { node, .. }
83            | AttributeEditState::Inputs { node, .. }
84            | AttributeEditState::Writing { node, .. } => node,
85        }
86    }
87
88    pub fn attr_name(&self) -> &str {
89        match self {
90            AttributeEditState::Loading { attr_name, .. }
91            | AttributeEditState::Failed { attr_name, .. }
92            | AttributeEditState::Inputs { attr_name, .. }
93            | AttributeEditState::Writing { attr_name, .. } => attr_name,
94        }
95    }
96}
97
98const MAX_LOG_LINES: usize = 1000;
99const MAX_HISTORY: usize = 20;
100
101#[derive(Debug, Clone, Copy, PartialEq, Eq)]
102pub enum ConnectionState {
103    Disconnected,
104    Connecting,
105    Connected,
106    Disconnecting,
107}
108
109#[derive(Debug, Clone, Copy, PartialEq, Eq)]
110pub enum DetailTab {
111    Attributes,
112    Events,
113    DataChanges,
114    Subscriptions,
115    References,
116}
117
118#[derive(Debug, Default)]
119pub struct TreeModel {
120    pub children: HashMap<NodeId, Vec<TreeChild>>,
121    pub expanded: HashSet<NodeId>,
122    pub loading: HashSet<NodeId>,
123}
124
125impl TreeModel {
126    pub fn clear(&mut self) {
127        self.children.clear();
128        self.expanded.clear();
129        self.loading.clear();
130    }
131}
132
133pub struct AppModel {
134    pub endpoint_url: String,
135    pub endpoint_history: Vec<String>,
136    pub connection: ConnectionState,
137    pub root_node: NodeId,
138    pub tree: TreeModel,
139    pub selected: Option<NodeId>,
140    pub node_summary: Option<NodeSummary>,
141    pub active_tab: DetailTab,
142    pub references: Option<Vec<ReferenceRow>>,
143    pub references_loading: bool,
144    pub log: VecDeque<LogLine>,
145    pub selected_endpoint: Option<EndpointInfo>,
146    pub endpoints_loading: bool,
147    pub discovered_endpoints: Option<Vec<EndpointInfo>>,
148    pub endpoints_dialog_open: bool,
149    pub auth_mode: AuthMode,
150    pub auth_username: String,
151    pub auth_password: String,
152    pub auth_cert_path: String,
153    pub auth_key_path: String,
154    pub last_selection_paths: HashMap<String, Vec<NodeId>>,
155    pub last_connection_selections: HashMap<String, ConnectionPrefs>,
156    pub endpoint_mode_filter: SecurityMode,
157    pub file_picker_open: bool,
158    pub method_call: Option<MethodCallState>,
159    pub subscriptions: Vec<SubscriptionRow>,
160    pub subscribing: HashSet<NodeId>,
161    pub attr_edit: Option<AttributeEditState>,
162}
163
164#[derive(Debug, Clone, Default)]
165pub struct ConnectionPrefs {
166    pub auth_mode: AuthMode,
167    pub security_mode: SecurityMode,
168    pub username: String,
169    pub cert_path: String,
170    pub key_path: String,
171}
172
173impl Default for AppModel {
174    fn default() -> Self {
175        Self {
176            endpoint_url: "opc.tcp://localhost:4855".to_string(),
177            endpoint_history: Vec::new(),
178            connection: ConnectionState::Disconnected,
179            root_node: NodeId::new(0, ObjectId::RootFolder as u32),
180            tree: TreeModel::default(),
181            selected: None,
182            node_summary: None,
183            active_tab: DetailTab::References,
184            references: None,
185            references_loading: false,
186            log: VecDeque::with_capacity(MAX_LOG_LINES),
187            selected_endpoint: None,
188            endpoints_loading: false,
189            discovered_endpoints: None,
190            endpoints_dialog_open: false,
191            auth_mode: AuthMode::Anonymous,
192            auth_username: String::new(),
193            auth_password: String::new(),
194            auth_cert_path: String::new(),
195            auth_key_path: String::new(),
196            last_selection_paths: HashMap::new(),
197            last_connection_selections: HashMap::new(),
198            endpoint_mode_filter: SecurityMode::None,
199            file_picker_open: false,
200            method_call: None,
201            subscriptions: Vec::new(),
202            subscribing: HashSet::new(),
203            attr_edit: None,
204        }
205    }
206}
207
208impl AppModel {
209    pub fn push_log(&mut self, line: LogLine) {
210        if self.log.len() == MAX_LOG_LINES {
211            self.log.pop_front();
212        }
213        self.log.push_back(line);
214    }
215
216    pub fn reset_session_state(&mut self) {
217        self.tree.clear();
218        self.selected = None;
219        self.node_summary = None;
220        self.references = None;
221        self.references_loading = false;
222        self.method_call = None;
223        self.subscriptions.clear();
224        self.subscribing.clear();
225        self.attr_edit = None;
226    }
227
228    pub fn record_successful_connection(&mut self) {
229        let url = self.endpoint_url.trim().to_string();
230        if url.is_empty() {
231            return;
232        }
233        self.endpoint_history.retain(|u| u != &url);
234        self.endpoint_history.insert(0, url.clone());
235        self.endpoint_history.truncate(MAX_HISTORY);
236        self.last_connection_selections.insert(
237            url,
238            ConnectionPrefs {
239                auth_mode: self.auth_mode,
240                security_mode: self.endpoint_mode_filter,
241                username: self.auth_username.clone(),
242                cert_path: self.auth_cert_path.clone(),
243                key_path: self.auth_key_path.clone(),
244            },
245        );
246    }
247
248    pub fn apply_saved_connection_prefs(&mut self) {
249        let Some(prefs) = self.last_connection_selections.get(&self.endpoint_url).cloned() else {
250            return;
251        };
252        self.auth_mode = prefs.auth_mode;
253        self.endpoint_mode_filter = prefs.security_mode;
254        self.auth_username = prefs.username;
255        self.auth_cert_path = prefs.cert_path;
256        self.auth_key_path = prefs.key_path;
257    }
258}