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}