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 endpoint_mode_filter: SecurityMode,
108 pub file_picker_open: bool,
109 pub method_call: Option<MethodCallState>,
110}
111
112impl Default for AppModel {
113 fn default() -> Self {
114 Self {
115 endpoint_url: "opc.tcp://localhost:4855".to_string(),
116 endpoint_history: Vec::new(),
117 connection: ConnectionState::Disconnected,
118 root_node: NodeId::new(0, ObjectId::RootFolder as u32),
119 tree: TreeModel::default(),
120 selected: None,
121 node_summary: None,
122 active_tab: DetailTab::References,
123 references: None,
124 references_loading: false,
125 log: VecDeque::with_capacity(MAX_LOG_LINES),
126 selected_endpoint: None,
127 endpoints_loading: false,
128 discovered_endpoints: None,
129 endpoints_dialog_open: false,
130 auth_mode: AuthMode::Anonymous,
131 auth_username: String::new(),
132 auth_password: String::new(),
133 auth_cert_path: String::new(),
134 auth_key_path: String::new(),
135 last_selection_paths: HashMap::new(),
136 endpoint_mode_filter: SecurityMode::None,
137 file_picker_open: false,
138 method_call: None,
139 }
140 }
141}
142
143impl AppModel {
144 pub fn push_log(&mut self, line: LogLine) {
145 if self.log.len() == MAX_LOG_LINES {
146 self.log.pop_front();
147 }
148 self.log.push_back(line);
149 }
150
151 pub fn reset_session_state(&mut self) {
152 self.tree.clear();
153 self.selected = None;
154 self.node_summary = None;
155 self.references = None;
156 self.references_loading = false;
157 self.method_call = None;
158 }
159
160 pub fn record_successful_connection(&mut self) {
161 let url = self.endpoint_url.trim().to_string();
162 if url.is_empty() {
163 return;
164 }
165 self.endpoint_history.retain(|u| u != &url);
166 self.endpoint_history.insert(0, url);
167 self.endpoint_history.truncate(MAX_HISTORY);
168 }
169}