Skip to main content

canlink_hal/filter/
config.rs

1//! Filter configuration (FR-006)
2//!
3//! Provides configuration structures for loading filters from TOML.
4
5use serde::Deserialize;
6
7use crate::error::FilterError;
8
9use super::{FilterChain, IdFilter, RangeFilter};
10
11/// Filter configuration from TOML
12///
13/// # Example TOML
14///
15/// ```toml
16/// [filters]
17/// [[filters.id_filters]]
18/// id = 0x123
19/// mask = 0x7FF
20/// extended = false
21///
22/// [[filters.range_filters]]
23/// start_id = 0x200
24/// end_id = 0x2FF
25/// ```
26#[derive(Debug, Clone, Default, Deserialize)]
27pub struct FilterConfig {
28    /// ID filters
29    #[serde(default)]
30    pub id_filters: Vec<IdFilterConfig>,
31
32    /// Range filters
33    #[serde(default)]
34    pub range_filters: Vec<RangeFilterConfig>,
35
36    /// Maximum hardware filters (default: 4)
37    #[serde(default = "default_max_hardware")]
38    pub max_hardware_filters: usize,
39}
40
41fn default_max_hardware() -> usize {
42    4
43}
44
45/// ID filter configuration
46#[derive(Debug, Clone, Deserialize)]
47pub struct IdFilterConfig {
48    /// Target ID
49    pub id: u32,
50
51    /// Mask (default: 0x7FF for standard, 0x1FFFFFFF for extended)
52    #[serde(default)]
53    pub mask: Option<u32>,
54
55    /// Extended frame (default: false)
56    #[serde(default)]
57    pub extended: bool,
58}
59
60/// Range filter configuration
61#[derive(Debug, Clone, Deserialize)]
62pub struct RangeFilterConfig {
63    /// Start ID (inclusive)
64    pub start_id: u32,
65
66    /// End ID (inclusive)
67    pub end_id: u32,
68
69    /// Extended frame (default: false)
70    #[serde(default)]
71    pub extended: bool,
72}
73
74impl FilterConfig {
75    /// Load configuration from TOML string
76    ///
77    /// # Errors
78    ///
79    /// Returns an error if the TOML string is invalid.
80    pub fn from_toml(toml_str: &str) -> Result<Self, toml::de::Error> {
81        toml::from_str(toml_str)
82    }
83
84    /// Build a `FilterChain` from this configuration
85    ///
86    /// # Errors
87    ///
88    /// Returns `FilterError` if any filter configuration is invalid.
89    pub fn into_chain(self) -> Result<FilterChain, FilterError> {
90        let mut chain = FilterChain::new(self.max_hardware_filters);
91
92        // Add ID filters
93        for config in self.id_filters {
94            let filter = if config.extended {
95                if let Some(mask) = config.mask {
96                    IdFilter::with_mask_extended(config.id, mask)
97                } else {
98                    IdFilter::try_new_extended(config.id)?
99                }
100            } else if let Some(mask) = config.mask {
101                IdFilter::with_mask(config.id, mask)
102            } else {
103                IdFilter::try_new(config.id)?
104            };
105            chain.add_filter(Box::new(filter));
106        }
107
108        // Add range filters
109        for config in self.range_filters {
110            let filter = if config.extended {
111                RangeFilter::try_new_extended(config.start_id, config.end_id)?
112            } else {
113                RangeFilter::try_new(config.start_id, config.end_id)?
114            };
115            chain.add_filter(Box::new(filter));
116        }
117
118        Ok(chain)
119    }
120}
121
122impl FilterChain {
123    /// Create a `FilterChain` from configuration
124    ///
125    /// # Errors
126    ///
127    /// Returns `FilterError` if any filter configuration is invalid.
128    pub fn from_config(config: &FilterConfig) -> Result<Self, FilterError> {
129        config.clone().into_chain()
130    }
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136    use crate::message::CanMessage;
137
138    #[test]
139    fn test_parse_id_filter() {
140        let toml = r"
141            [[id_filters]]
142            id = 0x123
143        ";
144
145        let config: FilterConfig = toml::from_str(toml).unwrap();
146        assert_eq!(config.id_filters.len(), 1);
147        assert_eq!(config.id_filters[0].id, 0x123);
148    }
149
150    #[test]
151    fn test_parse_id_filter_with_mask() {
152        let toml = r"
153            [[id_filters]]
154            id = 0x120
155            mask = 0x7F0
156        ";
157
158        let config: FilterConfig = toml::from_str(toml).unwrap();
159        assert_eq!(config.id_filters[0].mask, Some(0x7F0));
160    }
161
162    #[test]
163    fn test_parse_range_filter() {
164        let toml = r"
165            [[range_filters]]
166            start_id = 0x100
167            end_id = 0x1FF
168        ";
169
170        let config: FilterConfig = toml::from_str(toml).unwrap();
171        assert_eq!(config.range_filters.len(), 1);
172        assert_eq!(config.range_filters[0].start_id, 0x100);
173        assert_eq!(config.range_filters[0].end_id, 0x1FF);
174    }
175
176    #[test]
177    fn test_into_chain() {
178        let toml = r"
179            max_hardware_filters = 2
180
181            [[id_filters]]
182            id = 0x123
183
184            [[range_filters]]
185            start_id = 0x200
186            end_id = 0x2FF
187        ";
188
189        let config: FilterConfig = toml::from_str(toml).unwrap();
190        let chain = config.into_chain().unwrap();
191
192        assert_eq!(chain.len(), 2);
193
194        let msg_123 = CanMessage::new_standard(0x123, &[0u8; 8]).unwrap();
195        let msg_250 = CanMessage::new_standard(0x250, &[0u8; 8]).unwrap();
196        let msg_300 = CanMessage::new_standard(0x300, &[0u8; 8]).unwrap();
197
198        assert!(chain.matches(&msg_123));
199        assert!(chain.matches(&msg_250));
200        assert!(!chain.matches(&msg_300));
201    }
202
203    #[test]
204    fn test_extended_filters() {
205        let toml = r"
206            [[id_filters]]
207            id = 0x12345678
208            extended = true
209
210            [[range_filters]]
211            start_id = 0x10000
212            end_id = 0x1FFFF
213            extended = true
214        ";
215
216        let config: FilterConfig = toml::from_str(toml).unwrap();
217        let chain = config.into_chain().unwrap();
218
219        let msg_ext = CanMessage::new_extended(0x1234_5678, &[0u8; 8]).unwrap();
220        let msg_range = CanMessage::new_extended(0x15000, &[0u8; 8]).unwrap();
221
222        assert!(chain.matches(&msg_ext));
223        assert!(chain.matches(&msg_range));
224    }
225}