can_viewer/commands/
filter.rs

1//! Frame filtering and statistics - all computation in Rust.
2//!
3//! Replaces TypeScript `getFilteredFrames()` and `calculateFrameStats()`.
4
5use crate::dto::CanFrameDto;
6use crate::state::AppState;
7use serde::{Deserialize, Serialize};
8use std::collections::{HashMap, HashSet};
9use std::sync::Arc;
10
11// ─────────────────────────────────────────────────────────────────────────────
12// Filter Configuration
13// ─────────────────────────────────────────────────────────────────────────────
14
15/// Match status filter for DBC matching.
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
17#[serde(rename_all = "lowercase")]
18pub enum MatchStatus {
19    #[default]
20    All,
21    Matched,
22    Unmatched,
23}
24
25/// Filter configuration matching TypeScript `Filters` interface.
26#[derive(Debug, Clone, Serialize, Deserialize, Default)]
27#[serde(rename_all = "camelCase")]
28pub struct FilterConfig {
29    /// Minimum timestamp (inclusive)
30    pub time_min: Option<f64>,
31    /// Maximum timestamp (inclusive)
32    pub time_max: Option<f64>,
33    /// CAN IDs to include (empty = all)
34    #[serde(default)]
35    pub can_ids: Vec<u32>,
36    /// Message names to filter (substring match, lowercase)
37    #[serde(default)]
38    pub messages: Vec<String>,
39    /// Signal names to filter (substring match, lowercase)
40    #[serde(default)]
41    pub signals: Vec<String>,
42    /// Data pattern filter (e.g., "01 ?? FF")
43    pub data_pattern: Option<String>,
44    /// Channel to filter (exact match)
45    pub channel: Option<String>,
46    /// Match status (all, matched, unmatched)
47    #[serde(default)]
48    pub match_status: MatchStatus,
49}
50
51/// Result of filtering frames.
52#[derive(Debug, Clone, Serialize, Deserialize)]
53#[serde(rename_all = "camelCase")]
54pub struct FilterResult {
55    /// Filtered frames
56    pub frames: Vec<CanFrameDto>,
57    /// Total frames before filtering
58    pub total_count: usize,
59    /// Filtered frame count
60    pub filtered_count: usize,
61}
62
63/// Frame statistics.
64#[derive(Debug, Clone, Serialize, Deserialize)]
65#[serde(rename_all = "camelCase")]
66pub struct FrameStats {
67    /// Number of unique message IDs
68    pub unique_messages: usize,
69    /// Frames per second
70    pub frame_rate: f64,
71    /// Average delta time in milliseconds
72    pub avg_delta_ms: f64,
73    /// Estimated bus load percentage (assumes 500kbps CAN)
74    pub bus_load: f64,
75}
76
77/// Message ID counts for aggregation.
78#[derive(Debug, Clone, Serialize, Deserialize)]
79#[serde(rename_all = "camelCase")]
80pub struct MessageCount {
81    pub can_id: u32,
82    pub is_extended: bool,
83    pub count: u64,
84}
85
86/// DLC detection result.
87#[derive(Debug, Clone, Serialize, Deserialize)]
88#[serde(rename_all = "camelCase")]
89pub struct DlcDetectionResult {
90    pub detected_dlc: u8,
91    pub confidence: f64,
92    pub sample_count: usize,
93}
94
95// ─────────────────────────────────────────────────────────────────────────────
96// Public: Pattern Matching (exported for use by other modules)
97// ─────────────────────────────────────────────────────────────────────────────
98
99/// Parse data pattern into bytes and wildcards.
100/// Pattern format: "01 ?? FF" where ?? is wildcard.
101pub fn parse_data_pattern(pattern: &str) -> Vec<Option<u8>> {
102    pattern
103        .split_whitespace()
104        .map(|s| {
105            let s = s.to_uppercase();
106            if s == "??" || s == "XX" {
107                None // Wildcard
108            } else {
109                u8::from_str_radix(&s, 16).ok()
110            }
111        })
112        .collect()
113}
114
115/// Check if frame data matches pattern.
116pub fn match_data_pattern(data: &[u8], pattern: &[Option<u8>]) -> bool {
117    if pattern.len() > data.len() {
118        return false;
119    }
120
121    for (i, expected) in pattern.iter().enumerate() {
122        if let Some(expected_byte) = expected {
123            if data[i] != *expected_byte {
124                return false;
125            }
126        }
127        // None = wildcard, always matches
128    }
129    true
130}
131
132// ─────────────────────────────────────────────────────────────────────────────
133// DBC Message Info Cache (public for pro crate reuse)
134// ─────────────────────────────────────────────────────────────────────────────
135
136/// Cached message info for filtering.
137/// Public for use by pro crate's multi-DBC filter.
138pub struct DbcMessageInfo {
139    pub name: String,
140    pub signal_names: Vec<String>,
141}
142
143/// Message info cache type alias for convenience.
144pub type DbcMessageCache = HashMap<(u32, bool), DbcMessageInfo>;
145
146/// Build message info cache from a DBC for fast lookups.
147/// This is the core function that both base and pro versions use.
148pub fn build_message_cache_from_dbc(dbc: &dbc_rs::Dbc) -> DbcMessageCache {
149    let mut cache = HashMap::new();
150
151    for msg in dbc.messages().iter() {
152        let is_extended = msg.id() > 0x7FF;
153        let signal_names: Vec<String> = msg
154            .signals()
155            .iter()
156            .map(|s| s.name().to_lowercase())
157            .collect();
158
159        cache.insert(
160            (msg.id(), is_extended),
161            DbcMessageInfo {
162                name: msg.name().to_lowercase(),
163                signal_names,
164            },
165        );
166    }
167
168    cache
169}
170
171/// Build message info cache from AppState's DBC.
172fn build_message_cache(state: &AppState) -> DbcMessageCache {
173    let dbc_guard = state.dbc.lock();
174    match *dbc_guard {
175        Some(ref dbc) => build_message_cache_from_dbc(dbc),
176        None => HashMap::new(),
177    }
178}
179
180// ─────────────────────────────────────────────────────────────────────────────
181// Core Filter Logic (public for pro crate reuse)
182// ─────────────────────────────────────────────────────────────────────────────
183
184/// Filter frames using a pre-built message cache.
185/// This is the core filtering function that both base and pro versions use.
186pub fn filter_frames_with_cache(
187    frames: Vec<CanFrameDto>,
188    filters: &FilterConfig,
189    msg_cache: &DbcMessageCache,
190) -> FilterResult {
191    let total_count = frames.len();
192
193    // Build CAN ID set for O(1) lookup
194    let can_id_set: HashSet<u32> = filters.can_ids.iter().copied().collect();
195    let has_can_id_filter = !can_id_set.is_empty();
196
197    // Parse data pattern once
198    let data_pattern = filters.data_pattern.as_ref().map(|p| parse_data_pattern(p));
199    let has_data_pattern = data_pattern.as_ref().is_some_and(|p| !p.is_empty());
200
201    // Lowercase message/signal filters for case-insensitive matching
202    let message_filters: Vec<String> = filters.messages.iter().map(|s| s.to_lowercase()).collect();
203    let has_message_filter = !message_filters.is_empty();
204
205    let signal_filters: Vec<String> = filters.signals.iter().map(|s| s.to_lowercase()).collect();
206    let has_signal_filter = !signal_filters.is_empty();
207
208    // Check if we need DBC-related filters
209    let needs_dbc =
210        filters.match_status != MatchStatus::All || has_message_filter || has_signal_filter;
211
212    // Filter frames
213    let filtered: Vec<CanFrameDto> = frames
214        .into_iter()
215        .filter(|frame| {
216            // Time range filter
217            if let Some(min) = filters.time_min {
218                if frame.timestamp < min {
219                    return false;
220                }
221            }
222            if let Some(max) = filters.time_max {
223                if frame.timestamp > max {
224                    return false;
225                }
226            }
227
228            // CAN ID filter
229            if has_can_id_filter && !can_id_set.contains(&frame.can_id) {
230                return false;
231            }
232
233            // Channel filter
234            if let Some(ref ch) = filters.channel {
235                if frame.channel != *ch {
236                    return false;
237                }
238            }
239
240            // Data pattern filter
241            if has_data_pattern {
242                if let Some(ref pattern) = data_pattern {
243                    if !match_data_pattern(&frame.data, pattern) {
244                        return false;
245                    }
246                }
247            }
248
249            // DBC-related filters
250            if needs_dbc {
251                let key = (frame.can_id, frame.is_extended);
252                let msg_info = msg_cache.get(&key);
253                let has_match = msg_info.is_some();
254
255                // Match status filter
256                match filters.match_status {
257                    MatchStatus::All => {}
258                    MatchStatus::Matched => {
259                        if !has_match {
260                            return false;
261                        }
262                    }
263                    MatchStatus::Unmatched => {
264                        if has_match {
265                            return false;
266                        }
267                    }
268                }
269
270                // Message name filter
271                if has_message_filter {
272                    let Some(info) = msg_info else {
273                        return false;
274                    };
275                    if !message_filters.iter().any(|m| info.name.contains(m)) {
276                        return false;
277                    }
278                }
279
280                // Signal name filter
281                if has_signal_filter {
282                    let Some(info) = msg_info else {
283                        return false;
284                    };
285                    if !signal_filters
286                        .iter()
287                        .any(|s| info.signal_names.iter().any(|sn| sn.contains(s)))
288                    {
289                        return false;
290                    }
291                }
292            }
293
294            true
295        })
296        .collect();
297
298    let filtered_count = filtered.len();
299
300    FilterResult {
301        frames: filtered,
302        total_count,
303        filtered_count,
304    }
305}
306
307// ─────────────────────────────────────────────────────────────────────────────
308// Commands
309// ─────────────────────────────────────────────────────────────────────────────
310
311/// Filter frames based on filter configuration.
312/// All filtering logic runs in Rust - TS just displays results.
313#[tauri::command]
314pub fn filter_frames(
315    frames: Vec<CanFrameDto>,
316    filters: FilterConfig,
317    state: tauri::State<'_, Arc<AppState>>,
318) -> FilterResult {
319    // Build DBC message cache if needed
320    let needs_dbc = filters.match_status != MatchStatus::All
321        || !filters.messages.is_empty()
322        || !filters.signals.is_empty();
323
324    let msg_cache = if needs_dbc {
325        build_message_cache(&state)
326    } else {
327        HashMap::new()
328    };
329
330    filter_frames_with_cache(frames, &filters, &msg_cache)
331}
332
333/// Calculate frame statistics.
334/// All computation in Rust - TS just displays results.
335#[tauri::command]
336pub fn calculate_frame_stats(frames: Vec<CanFrameDto>) -> FrameStats {
337    if frames.is_empty() {
338        return FrameStats {
339            unique_messages: 0,
340            frame_rate: 0.0,
341            avg_delta_ms: 0.0,
342            bus_load: 0.0,
343        };
344    }
345
346    // Count unique message IDs
347    let unique_ids: HashSet<u32> = frames.iter().map(|f| f.can_id).collect();
348    let unique_messages = unique_ids.len();
349
350    // Calculate frame rate and delta time using recent frames
351    let recent_count = frames.len().min(100);
352    let recent_frames = &frames[frames.len() - recent_count..];
353
354    let mut frame_rate = 0.0;
355    let mut avg_delta_ms = 0.0;
356
357    if recent_frames.len() >= 2 {
358        let first_ts = recent_frames[0].timestamp;
359        let last_ts = recent_frames[recent_frames.len() - 1].timestamp;
360        let duration = last_ts - first_ts;
361
362        if duration > 0.0 {
363            frame_rate = (recent_frames.len() - 1) as f64 / duration;
364            avg_delta_ms = (duration / (recent_frames.len() - 1) as f64) * 1000.0;
365        }
366    }
367
368    // Calculate bus load (approximate)
369    // Assumes 500kbps CAN, average frame ~100 bits
370    // Bus capacity at 500kbps ≈ 5000 frames/sec theoretical max
371    let max_frames_per_sec = 5000.0;
372    let bus_load = (frame_rate / max_frames_per_sec * 100.0).min(100.0);
373
374    FrameStats {
375        unique_messages,
376        frame_rate,
377        avg_delta_ms,
378        bus_load,
379    }
380}
381
382/// Get message ID counts (sorted by count descending).
383/// Replaces TypeScript `updateAvailableIds()`.
384#[tauri::command]
385pub fn get_message_counts(frames: Vec<CanFrameDto>) -> Vec<MessageCount> {
386    let mut counts: HashMap<(u32, bool), u64> = HashMap::new();
387
388    for frame in &frames {
389        *counts.entry((frame.can_id, frame.is_extended)).or_default() += 1;
390    }
391
392    let mut result: Vec<MessageCount> = counts
393        .into_iter()
394        .map(|((can_id, is_extended), count)| MessageCount {
395            can_id,
396            is_extended,
397            count,
398        })
399        .collect();
400
401    // Sort by count descending
402    result.sort_by(|a, b| b.count.cmp(&a.count));
403
404    result
405}
406
407/// Detect DLC from frames for a specific CAN ID.
408/// Uses histogram to find most common DLC value.
409#[tauri::command]
410pub fn detect_dlc(frames: Vec<CanFrameDto>, can_id: u32, is_extended: bool) -> DlcDetectionResult {
411    // Filter frames for this CAN ID
412    let matching: Vec<&CanFrameDto> = frames
413        .iter()
414        .filter(|f| f.can_id == can_id && f.is_extended == is_extended)
415        .collect();
416
417    if matching.is_empty() {
418        return DlcDetectionResult {
419            detected_dlc: 8,
420            confidence: 0.0,
421            sample_count: 0,
422        };
423    }
424
425    // Build histogram of DLC values
426    let mut dlc_counts: HashMap<u8, usize> = HashMap::new();
427    for frame in &matching {
428        *dlc_counts.entry(frame.dlc).or_default() += 1;
429    }
430
431    // Find most common DLC
432    let (detected_dlc, max_count) = dlc_counts
433        .iter()
434        .max_by_key(|(_, count)| *count)
435        .map(|(dlc, count)| (*dlc, *count))
436        .unwrap_or((8, 0));
437
438    let confidence = if matching.is_empty() {
439        0.0
440    } else {
441        max_count as f64 / matching.len() as f64
442    };
443
444    DlcDetectionResult {
445        detected_dlc,
446        confidence,
447        sample_count: matching.len(),
448    }
449}
450
451// ─────────────────────────────────────────────────────────────────────────────
452// Tests
453// ─────────────────────────────────────────────────────────────────────────────
454
455#[cfg(test)]
456mod tests {
457    use super::*;
458
459    fn make_frame(id: u32, timestamp: f64, data: Vec<u8>) -> CanFrameDto {
460        CanFrameDto {
461            timestamp,
462            channel: "vcan0".to_string(),
463            can_id: id,
464            is_extended: false,
465            is_fd: false,
466            brs: false,
467            esi: false,
468            dlc: data.len() as u8,
469            data,
470        }
471    }
472
473    #[test]
474    fn test_parse_data_pattern() {
475        let pattern = parse_data_pattern("01 ?? FF");
476        assert_eq!(pattern.len(), 3);
477        assert_eq!(pattern[0], Some(0x01));
478        assert_eq!(pattern[1], None); // Wildcard
479        assert_eq!(pattern[2], Some(0xFF));
480    }
481
482    #[test]
483    fn test_match_data_pattern() {
484        let pattern = parse_data_pattern("01 ?? FF");
485        assert!(match_data_pattern(&[0x01, 0x00, 0xFF], &pattern));
486        assert!(match_data_pattern(&[0x01, 0xAB, 0xFF], &pattern));
487        assert!(!match_data_pattern(&[0x01, 0xAB, 0xFE], &pattern));
488        assert!(!match_data_pattern(&[0x02, 0xAB, 0xFF], &pattern));
489    }
490
491    #[test]
492    fn test_calculate_frame_stats_empty() {
493        let stats = calculate_frame_stats(vec![]);
494        assert_eq!(stats.unique_messages, 0);
495        assert_eq!(stats.frame_rate, 0.0);
496    }
497
498    #[test]
499    fn test_calculate_frame_stats() {
500        let frames = vec![
501            make_frame(0x100, 0.0, vec![0, 1, 2, 3, 4, 5, 6, 7]),
502            make_frame(0x100, 0.001, vec![0, 1, 2, 3, 4, 5, 6, 7]),
503            make_frame(0x200, 0.002, vec![0, 1, 2, 3, 4, 5, 6, 7]),
504        ];
505        let stats = calculate_frame_stats(frames);
506        assert_eq!(stats.unique_messages, 2);
507        assert!(stats.frame_rate > 0.0);
508    }
509
510    #[test]
511    fn test_get_message_counts() {
512        let frames = vec![
513            make_frame(0x100, 0.0, vec![]),
514            make_frame(0x100, 0.001, vec![]),
515            make_frame(0x200, 0.002, vec![]),
516        ];
517        let counts = get_message_counts(frames);
518        assert_eq!(counts.len(), 2);
519        assert_eq!(counts[0].can_id, 0x100); // Higher count first
520        assert_eq!(counts[0].count, 2);
521        assert_eq!(counts[1].can_id, 0x200);
522        assert_eq!(counts[1].count, 1);
523    }
524
525    #[test]
526    fn test_detect_dlc() {
527        let frames = vec![
528            make_frame(0x100, 0.0, vec![0; 8]),
529            make_frame(0x100, 0.001, vec![0; 8]),
530            make_frame(0x100, 0.002, vec![0; 4]),
531            make_frame(0x200, 0.003, vec![0; 2]),
532        ];
533        let result = detect_dlc(frames, 0x100, false);
534        assert_eq!(result.detected_dlc, 8);
535        assert!(result.confidence > 0.6); // 2/3 = 0.666
536        assert_eq!(result.sample_count, 3);
537    }
538}