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, 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}