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    Reconnecting,
107    Disconnecting,
108}
109
110#[derive(Debug, Clone, Copy, PartialEq, Eq)]
111pub enum DetailTab {
112    Attributes,
113    Events,
114    DataChanges,
115    Subscriptions,
116    References,
117}
118
119#[derive(Debug, Default)]
120pub struct TreeModel {
121    pub children: HashMap<NodeId, Vec<TreeChild>>,
122    pub expanded: HashSet<NodeId>,
123    pub loading: HashSet<NodeId>,
124}
125
126impl TreeModel {
127    pub fn clear(&mut self) {
128        self.children.clear();
129        self.expanded.clear();
130        self.loading.clear();
131    }
132}
133
134pub struct AppModel {
135    pub endpoint_url: String,
136    pub endpoint_history: Vec<String>,
137    pub connection: ConnectionState,
138    pub root_node: NodeId,
139    pub tree: TreeModel,
140    pub selected: Option<NodeId>,
141    pub node_summary: Option<NodeSummary>,
142    pub active_tab: DetailTab,
143    pub references: Option<Vec<ReferenceRow>>,
144    pub references_loading: bool,
145    pub log: VecDeque<LogLine>,
146    pub selected_endpoint: Option<EndpointInfo>,
147    pub endpoints_loading: bool,
148    pub discovered_endpoints: Option<Vec<EndpointInfo>>,
149    pub endpoints_dialog_open: bool,
150    pub auth_mode: AuthMode,
151    pub auth_username: String,
152    pub auth_password: String,
153    pub auth_cert_path: String,
154    pub auth_key_path: String,
155    pub last_selection_paths: HashMap<String, Vec<NodeId>>,
156    pub last_connection_selections: HashMap<String, ConnectionPrefs>,
157    pub endpoint_mode_filter: SecurityMode,
158    pub file_picker_open: bool,
159    pub method_call: Option<MethodCallState>,
160    pub subscriptions: Vec<SubscriptionRow>,
161    pub subscribing: HashSet<NodeId>,
162    pub attr_edit: Option<AttributeEditState>,
163}
164
165#[derive(Debug, Clone, Default)]
166pub struct ConnectionPrefs {
167    pub auth_mode: AuthMode,
168    pub security_mode: SecurityMode,
169    pub username: String,
170    pub cert_path: String,
171    pub key_path: String,
172}
173
174impl Default for AppModel {
175    fn default() -> Self {
176        Self {
177            endpoint_url: "opc.tcp://localhost:4855".to_string(),
178            endpoint_history: Vec::new(),
179            connection: ConnectionState::Disconnected,
180            root_node: NodeId::new(0, ObjectId::RootFolder as u32),
181            tree: TreeModel::default(),
182            selected: None,
183            node_summary: None,
184            active_tab: DetailTab::References,
185            references: None,
186            references_loading: false,
187            log: VecDeque::with_capacity(MAX_LOG_LINES),
188            selected_endpoint: None,
189            endpoints_loading: false,
190            discovered_endpoints: None,
191            endpoints_dialog_open: false,
192            auth_mode: AuthMode::Anonymous,
193            auth_username: String::new(),
194            auth_password: String::new(),
195            auth_cert_path: String::new(),
196            auth_key_path: String::new(),
197            last_selection_paths: HashMap::new(),
198            last_connection_selections: HashMap::new(),
199            endpoint_mode_filter: SecurityMode::None,
200            file_picker_open: false,
201            method_call: None,
202            subscriptions: Vec::new(),
203            subscribing: HashSet::new(),
204            attr_edit: None,
205        }
206    }
207}
208
209impl AppModel {
210    pub fn push_log(&mut self, line: LogLine) {
211        if self.log.len() == MAX_LOG_LINES {
212            self.log.pop_front();
213        }
214        self.log.push_back(line);
215    }
216
217    pub fn reset_session_state(&mut self) {
218        self.tree.clear();
219        self.selected = None;
220        self.node_summary = None;
221        self.references = None;
222        self.references_loading = false;
223        self.method_call = None;
224        self.subscriptions.clear();
225        self.subscribing.clear();
226        self.attr_edit = None;
227    }
228
229    pub fn record_successful_connection(&mut self) {
230        let url = self.endpoint_url.trim().to_string();
231        if url.is_empty() {
232            return;
233        }
234        self.endpoint_history.retain(|u| u != &url);
235        self.endpoint_history.insert(0, url.clone());
236        self.endpoint_history.truncate(MAX_HISTORY);
237        self.last_connection_selections.insert(
238            url,
239            ConnectionPrefs {
240                auth_mode: self.auth_mode,
241                security_mode: self.endpoint_mode_filter,
242                username: self.auth_username.clone(),
243                cert_path: self.auth_cert_path.clone(),
244                key_path: self.auth_key_path.clone(),
245            },
246        );
247    }
248
249    pub fn apply_saved_connection_prefs(&mut self) {
250        let Some(prefs) = self.last_connection_selections.get(&self.endpoint_url).cloned() else {
251            return;
252        };
253        self.auth_mode = prefs.auth_mode;
254        self.endpoint_mode_filter = prefs.security_mode;
255        self.auth_username = prefs.username;
256        self.auth_cert_path = prefs.cert_path;
257        self.auth_key_path = prefs.key_path;
258    }
259}