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}