can_viewer/commands/
dbc.rs

1//! DBC file loading and management commands.
2
3use crate::decode::{DecodeResult, decode_frame};
4use crate::dto::{CanFrameDto, DecodeResponse};
5use crate::state::AppState;
6use dbc_rs::Dbc;
7use serde::Serialize;
8use std::sync::Arc;
9use tauri::State;
10
11/// Signal definition from DBC.
12#[derive(Debug, Clone, Serialize)]
13pub struct SignalInfo {
14    pub name: String,
15    pub start_bit: u32,
16    pub length: u32,
17    pub byte_order: String,
18    pub is_signed: bool,
19    pub factor: f64,
20    pub offset: f64,
21    pub min: f64,
22    pub max: f64,
23    pub unit: String,
24    /// Comment from CM_ SG_ entry
25    pub comment: Option<String>,
26}
27
28/// Message definition from DBC.
29#[derive(Debug, Clone, Serialize)]
30pub struct MessageInfo {
31    pub id: u32,
32    pub name: String,
33    pub dlc: u8,
34    pub sender: String,
35    pub signals: Vec<SignalInfo>,
36    /// Comment from CM_ BO_ entry
37    pub comment: Option<String>,
38}
39
40/// Full DBC structure for display.
41#[derive(Debug, Clone, Serialize)]
42pub struct DbcInfo {
43    pub messages: Vec<MessageInfo>,
44}
45
46/// Load and parse a DBC file.
47/// Saves the path to session config for persistence.
48#[tauri::command]
49pub async fn load_dbc(path: String, state: State<'_, Arc<AppState>>) -> Result<String, String> {
50    let content =
51        std::fs::read_to_string(&path).map_err(|e| format!("Failed to read DBC: {}", e))?;
52
53    let dbc = Dbc::parse(&content).map_err(|e| format!("Failed to parse DBC: {:?}", e))?;
54    let msg_count = dbc.messages().len();
55
56    state.set_dbc(dbc);
57    *state.dbc_path.lock() = Some(path.clone());
58
59    // Save to session config for persistence
60    if let Err(e) = state.session.lock().set_dbc_path(Some(path.clone())) {
61        log::warn!("Failed to save session: {}", e);
62    }
63
64    Ok(format!("Loaded {} messages", msg_count))
65}
66
67/// Clear the loaded DBC data.
68/// Removes from session config.
69#[tauri::command]
70pub async fn clear_dbc(state: State<'_, Arc<AppState>>) -> Result<(), String> {
71    state.clear_dbc();
72    *state.dbc_path.lock() = None;
73
74    // Clear from session config
75    if let Err(e) = state.session.lock().set_dbc_path(None) {
76        log::warn!("Failed to save session: {}", e);
77    }
78
79    Ok(())
80}
81
82/// Get the path to the currently loaded DBC file.
83#[tauri::command]
84pub async fn get_dbc_path(state: State<'_, Arc<AppState>>) -> Result<Option<String>, String> {
85    Ok(state.dbc_path.lock().clone())
86}
87
88/// Decode a single CAN frame using the loaded DBC.
89#[tauri::command]
90pub async fn decode_single_frame(
91    frame: CanFrameDto,
92    state: State<'_, Arc<AppState>>,
93) -> Result<DecodeResponse, String> {
94    let dbc_guard = state.dbc.lock();
95    let Some(ref dbc) = *dbc_guard else {
96        return Ok(DecodeResponse {
97            signals: Vec::new(),
98            errors: Vec::new(),
99        });
100    };
101
102    match decode_frame(&frame, dbc) {
103        DecodeResult::Signals(signals) => Ok(DecodeResponse {
104            signals,
105            errors: Vec::new(),
106        }),
107        DecodeResult::Error(err) => Ok(DecodeResponse {
108            signals: Vec::new(),
109            errors: vec![err],
110        }),
111    }
112}
113
114/// Decode multiple CAN frames using the loaded DBC.
115#[tauri::command]
116pub async fn decode_frames(
117    frames: Vec<CanFrameDto>,
118    state: State<'_, Arc<AppState>>,
119) -> Result<DecodeResponse, String> {
120    let dbc_guard = state.dbc.lock();
121    let Some(ref dbc) = *dbc_guard else {
122        return Ok(DecodeResponse {
123            signals: Vec::new(),
124            errors: Vec::new(),
125        });
126    };
127
128    let mut signals = Vec::new();
129    let mut errors = Vec::new();
130
131    for frame in &frames {
132        match decode_frame(frame, dbc) {
133            DecodeResult::Signals(sigs) => signals.extend(sigs),
134            DecodeResult::Error(err) => errors.push(err),
135        }
136    }
137
138    Ok(DecodeResponse { signals, errors })
139}
140
141/// Save DBC content to a file.
142/// Validates the content by parsing it before writing.
143#[tauri::command]
144pub async fn save_dbc_content(
145    path: String,
146    content: String,
147    state: State<'_, Arc<AppState>>,
148) -> Result<(), String> {
149    // Validate by parsing the content BEFORE writing to file
150    let dbc = Dbc::parse(&content).map_err(|e| format!("Invalid DBC content: {:?}", e))?;
151
152    // Content is valid, now write to file
153    std::fs::write(&path, &content).map_err(|e| format!("Failed to write DBC: {}", e))?;
154
155    // Update state with the parsed DBC
156    state.set_dbc(dbc);
157    *state.dbc_path.lock() = Some(path.clone());
158
159    // Save to session config
160    if let Err(e) = state.session.lock().set_dbc_path(Some(path.clone())) {
161        log::warn!("Failed to save session: {}", e);
162    }
163
164    Ok(())
165}
166
167/// Update the in-memory DBC from content string (for live editing).
168/// Does NOT save to file or update the file path.
169#[tauri::command]
170pub async fn update_dbc_content(
171    content: String,
172    state: State<'_, Arc<AppState>>,
173) -> Result<String, String> {
174    let dbc = Dbc::parse(&content).map_err(|e| format!("Failed to parse DBC: {:?}", e))?;
175    let msg_count = dbc.messages().len();
176
177    state.set_dbc(dbc);
178    Ok(format!("Updated DBC with {} messages", msg_count))
179}
180
181/// Get information about the loaded DBC.
182#[tauri::command]
183pub async fn get_dbc_info(state: State<'_, Arc<AppState>>) -> Result<Option<DbcInfo>, String> {
184    let dbc_guard = state.dbc.lock();
185    let Some(ref dbc) = *dbc_guard else {
186        return Ok(None);
187    };
188
189    let messages: Vec<MessageInfo> = dbc
190        .messages()
191        .iter()
192        .map(|msg| {
193            let signals: Vec<SignalInfo> = msg
194                .signals()
195                .iter()
196                .map(|sig| {
197                    let byte_order = match sig.byte_order() {
198                        dbc_rs::ByteOrder::BigEndian => "big_endian",
199                        dbc_rs::ByteOrder::LittleEndian => "little_endian",
200                    };
201                    SignalInfo {
202                        name: sig.name().to_string(),
203                        start_bit: sig.start_bit() as u32,
204                        length: sig.length() as u32,
205                        byte_order: byte_order.to_string(),
206                        is_signed: !sig.is_unsigned(),
207                        factor: sig.factor(),
208                        offset: sig.offset(),
209                        min: sig.min(),
210                        max: sig.max(),
211                        unit: sig.unit().unwrap_or("").to_string(),
212                        comment: sig.comment().map(|s| s.to_string()),
213                    }
214                })
215                .collect();
216
217            MessageInfo {
218                id: msg.id(),
219                name: msg.name().to_string(),
220                dlc: msg.dlc(),
221                sender: msg.sender().to_string(),
222                signals,
223                comment: msg.comment().map(|s| s.to_string()),
224            }
225        })
226        .collect();
227
228    Ok(Some(DbcInfo { messages }))
229}