dbc_rs/dbc/
encode.rs

1use crate::{Dbc, Error, Message, Result, compat::Vec};
2
3/// Maximum CAN FD payload size (64 bytes)
4const MAX_PAYLOAD_SIZE: usize = 64;
5
6impl Dbc {
7    /// Encode signal values into a CAN message payload.
8    ///
9    /// This is the inverse of [`Dbc::decode()`]. It takes a list of signal names
10    /// and their physical values, and produces a raw CAN message payload ready
11    /// for transmission.
12    ///
13    /// # Arguments
14    ///
15    /// * `id` - The raw CAN message ID (without extended flag)
16    /// * `signals` - Slice of (signal_name, physical_value) tuples to encode
17    /// * `is_extended` - Whether this is an extended (29-bit) CAN ID
18    ///
19    /// # Returns
20    ///
21    /// * `Ok(Vec<u8, 64>)` - The encoded payload, sized according to message DLC
22    /// * `Err(Error)` - If message not found, signal not found, or value out of range
23    ///
24    /// # Examples
25    ///
26    /// ```rust,no_run
27    /// use dbc_rs::Dbc;
28    ///
29    /// let dbc = Dbc::parse(r#"VERSION "1.0"
30    ///
31    /// BU_: ECM
32    ///
33    /// BO_ 256 Engine : 8 ECM
34    ///  SG_ RPM : 0|16@1+ (0.25,0) [0|8000] "rpm" *
35    ///  SG_ Temp : 16|8@1- (1,-40) [-40|215] "°C" *
36    /// "#)?;
37    ///
38    /// // Encode RPM=2000 and Temp=50°C
39    /// let payload = dbc.encode(256, &[("RPM", 2000.0), ("Temp", 50.0)], false)?;
40    ///
41    /// // The payload can now be transmitted over CAN
42    /// assert_eq!(payload.len(), 8);
43    /// # Ok::<(), dbc_rs::Error>(())
44    /// ```
45    ///
46    /// # Multiplexing
47    ///
48    /// For multiplexed messages, include the multiplexer switch signal in the
49    /// signal list along with the multiplexed signals you want to encode:
50    ///
51    /// ```rust,no_run
52    /// use dbc_rs::Dbc;
53    ///
54    /// let dbc = Dbc::parse(r#"VERSION "1.0"
55    ///
56    /// BU_: ECM
57    ///
58    /// BO_ 300 Sensors : 8 ECM
59    ///  SG_ SensorID M : 0|8@1+ (1,0) [0|3] ""
60    ///  SG_ Temperature m0 : 8|16@1- (0.1,-40) [-40|125] "°C" *
61    ///  SG_ Pressure m1 : 8|16@1+ (0.01,0) [0|655.35] "kPa" *
62    /// "#)?;
63    ///
64    /// // Encode temperature reading (SensorID=0)
65    /// let payload = dbc.encode(300, &[("SensorID", 0.0), ("Temperature", 25.0)], false)?;
66    /// # Ok::<(), dbc_rs::Error>(())
67    /// ```
68    #[inline]
69    pub fn encode(
70        &self,
71        id: u32,
72        signals: &[(&str, f64)],
73        is_extended: bool,
74    ) -> Result<Vec<u8, MAX_PAYLOAD_SIZE>> {
75        // If it's an extended ID, add the extended ID flag
76        let id = if is_extended {
77            id | Message::EXTENDED_ID_FLAG
78        } else {
79            id
80        };
81
82        // Find message by ID
83        let message = self
84            .messages()
85            .find_by_id(id)
86            .ok_or(Error::Encoding(Error::MESSAGE_NOT_FOUND))?;
87
88        // Create zero-initialized payload of size DLC
89        let dlc = message.dlc() as usize;
90        let mut payload: Vec<u8, MAX_PAYLOAD_SIZE> = Vec::new();
91        for _ in 0..dlc {
92            payload.push(0).map_err(|_| Error::Encoding(Error::MESSAGE_DLC_TOO_LARGE))?;
93        }
94
95        // Encode each signal
96        for &(signal_name, physical_value) in signals {
97            // Find signal in message
98            let signal = message
99                .signals()
100                .iter()
101                .find(|s| s.name() == signal_name)
102                .ok_or(Error::Encoding(Error::ENCODING_SIGNAL_NOT_FOUND))?;
103
104            // Encode and insert into payload (get mutable slice for heapless compatibility)
105            signal.encode_to(physical_value, payload.as_mut_slice())?;
106        }
107
108        Ok(payload)
109    }
110
111    /// Encode signal values into a CAN frame using embedded-can types.
112    ///
113    /// This is a convenience method that encodes signals and returns the result
114    /// in a format compatible with embedded-can drivers.
115    ///
116    /// # Arguments
117    ///
118    /// * `id` - The CAN ID (Standard or Extended)
119    /// * `signals` - Slice of (signal_name, physical_value) tuples to encode
120    ///
121    /// # Returns
122    ///
123    /// * `Ok(Vec<u8, 64>)` - The encoded payload
124    /// * `Err(Error)` - If encoding fails
125    ///
126    /// # Feature
127    ///
128    /// This method is only available when the `embedded-can` feature is enabled.
129    #[cfg(feature = "embedded-can")]
130    #[inline]
131    pub fn encode_for_id(
132        &self,
133        id: embedded_can::Id,
134        signals: &[(&str, f64)],
135    ) -> Result<Vec<u8, MAX_PAYLOAD_SIZE>> {
136        match id {
137            embedded_can::Id::Standard(std_id) => {
138                self.encode(std_id.as_raw() as u32, signals, false)
139            }
140            embedded_can::Id::Extended(ext_id) => self.encode(ext_id.as_raw(), signals, true),
141        }
142    }
143}
144
145#[cfg(test)]
146mod tests {
147    use crate::Dbc;
148
149    #[test]
150    fn test_encode_basic() {
151        let dbc = Dbc::parse(
152            r#"VERSION "1.0"
153
154BU_: ECM
155
156BO_ 256 Engine : 8 ECM
157 SG_ RPM : 0|16@1+ (0.25,0) [0|8000] "rpm" *
158"#,
159        )
160        .unwrap();
161
162        // Encode RPM = 2000.0 (raw: 2000 / 0.25 = 8000 = 0x1F40)
163        let payload = dbc.encode(256, &[("RPM", 2000.0)], false).unwrap();
164        assert_eq!(payload.len(), 8);
165        // Little-endian 16-bit: 8000 = 0x1F40 -> [0x40, 0x1F]
166        assert_eq!(payload[0], 0x40);
167        assert_eq!(payload[1], 0x1F);
168    }
169
170    #[test]
171    fn test_encode_multiple_signals() {
172        let dbc = Dbc::parse(
173            r#"VERSION "1.0"
174
175BU_: ECM
176
177BO_ 256 Engine : 8 ECM
178 SG_ RPM : 0|16@1+ (0.25,0) [0|8000] "rpm" *
179 SG_ Temp : 16|8@1- (1,-40) [-40|215] "°C" *
180"#,
181        )
182        .unwrap();
183
184        // Encode RPM = 2000.0 and Temp = 50°C
185        // RPM: 2000 / 0.25 = 8000 = 0x1F40
186        // Temp: (50 - (-40)) / 1 = 90 = 0x5A
187        let payload = dbc.encode(256, &[("RPM", 2000.0), ("Temp", 50.0)], false).unwrap();
188        assert_eq!(payload.len(), 8);
189        assert_eq!(payload[0], 0x40); // RPM low byte
190        assert_eq!(payload[1], 0x1F); // RPM high byte
191        assert_eq!(payload[2], 0x5A); // Temp
192    }
193
194    #[test]
195    fn test_encode_decode_roundtrip() {
196        let dbc = Dbc::parse(
197            r#"VERSION "1.0"
198
199BU_: ECM
200
201BO_ 256 Engine : 8 ECM
202 SG_ RPM : 0|16@1+ (0.25,0) [0|8000] "rpm" *
203 SG_ Temp : 16|8@1- (1,-40) [-40|215] "°C" *
204 SG_ Throttle : 24|8@1+ (1,0) [0|100] "%" *
205"#,
206        )
207        .unwrap();
208
209        // Original values
210        let rpm = 3500.0;
211        let temp = 85.0;
212        let throttle = 42.0;
213
214        // Encode
215        let payload = dbc
216            .encode(
217                256,
218                &[("RPM", rpm), ("Temp", temp), ("Throttle", throttle)],
219                false,
220            )
221            .unwrap();
222
223        // Decode
224        let decoded = dbc.decode(256, &payload, false).unwrap();
225
226        // Verify roundtrip
227        let find_value = |name: &str| decoded.iter().find(|s| s.name == name).map(|s| s.value);
228        assert!((find_value("RPM").unwrap() - rpm).abs() < 0.5); // Within factor precision
229        assert!((find_value("Temp").unwrap() - temp).abs() < 0.5);
230        assert!((find_value("Throttle").unwrap() - throttle).abs() < 0.5);
231    }
232
233    #[test]
234    fn test_encode_message_not_found() {
235        let dbc = Dbc::parse(
236            r#"VERSION "1.0"
237
238BU_: ECM
239
240BO_ 256 Engine : 8 ECM
241 SG_ RPM : 0|16@1+ (0.25,0) [0|8000] "rpm" *
242"#,
243        )
244        .unwrap();
245
246        let result = dbc.encode(512, &[("RPM", 2000.0)], false);
247        assert!(result.is_err());
248    }
249
250    #[test]
251    fn test_encode_signal_not_found() {
252        let dbc = Dbc::parse(
253            r#"VERSION "1.0"
254
255BU_: ECM
256
257BO_ 256 Engine : 8 ECM
258 SG_ RPM : 0|16@1+ (0.25,0) [0|8000] "rpm" *
259"#,
260        )
261        .unwrap();
262
263        let result = dbc.encode(256, &[("NonExistent", 100.0)], false);
264        assert!(result.is_err());
265    }
266
267    #[test]
268    fn test_encode_value_out_of_range() {
269        let dbc = Dbc::parse(
270            r#"VERSION "1.0"
271
272BU_: ECM
273
274BO_ 256 Engine : 8 ECM
275 SG_ RPM : 0|16@1+ (0.25,0) [0|8000] "rpm" *
276"#,
277        )
278        .unwrap();
279
280        // Value above max (8000)
281        let result = dbc.encode(256, &[("RPM", 9000.0)], false);
282        assert!(result.is_err());
283
284        // Value below min (0)
285        let result = dbc.encode(256, &[("RPM", -100.0)], false);
286        assert!(result.is_err());
287    }
288
289    #[test]
290    fn test_encode_signed_signal() {
291        let dbc = Dbc::parse(
292            r#"VERSION "1.0"
293
294BU_: ECM
295
296BO_ 256 Engine : 8 ECM
297 SG_ Torque : 0|16@1- (0.01,0) [-327.68|327.67] "Nm" *
298"#,
299        )
300        .unwrap();
301
302        // Encode negative value: -10.0 -> raw = -1000 = 0xFC18 (two's complement)
303        let payload = dbc.encode(256, &[("Torque", -10.0)], false).unwrap();
304        assert_eq!(payload[0], 0x18); // Low byte
305        assert_eq!(payload[1], 0xFC); // High byte (two's complement)
306
307        // Roundtrip verify
308        let decoded = dbc.decode(256, &payload, false).unwrap();
309        assert!((decoded[0].value - (-10.0)).abs() < 0.01);
310    }
311
312    #[test]
313    fn test_encode_big_endian() {
314        let dbc = Dbc::parse(
315            r#"VERSION "1.0"
316
317BU_: ECM
318
319BO_ 256 Engine : 8 ECM
320 SG_ Pressure : 7|16@0+ (0.01,0) [0|655.35] "kPa" *
321"#,
322        )
323        .unwrap();
324
325        // Encode 10.0 kPa -> raw = 1000 = 0x03E8, big-endian at bit 7
326        let payload = dbc.encode(256, &[("Pressure", 10.0)], false).unwrap();
327        assert_eq!(payload[0], 0x03); // High byte first (big-endian)
328        assert_eq!(payload[1], 0xE8); // Low byte
329
330        // Roundtrip verify
331        let decoded = dbc.decode(256, &payload, false).unwrap();
332        assert!((decoded[0].value - 10.0).abs() < 0.01);
333    }
334
335    #[test]
336    fn test_encode_extended_can_id() {
337        // 0x80000000 + 0x400 = 2147484672
338        let dbc = Dbc::parse(
339            r#"VERSION "1.0"
340
341BU_: ECM
342
343BO_ 2147484672 ExtendedMsg : 8 ECM
344 SG_ Speed : 0|16@1+ (0.1,0) [0|6553.5] "km/h" *
345"#,
346        )
347        .unwrap();
348
349        // Encode 100.0 km/h with extended ID 0x400
350        let payload = dbc.encode(0x400, &[("Speed", 100.0)], true).unwrap();
351        assert_eq!(payload[0], 0xE8); // 1000 = 0x03E8
352        assert_eq!(payload[1], 0x03);
353
354        // Roundtrip verify
355        let decoded = dbc.decode(0x400, &payload, true).unwrap();
356        assert!((decoded[0].value - 100.0).abs() < 0.1);
357    }
358
359    #[test]
360    fn test_encode_multiplexed_signal() {
361        let dbc = Dbc::parse(
362            r#"VERSION "1.0"
363
364BU_: ECM
365
366BO_ 300 Sensors : 8 ECM
367 SG_ SensorID M : 0|8@1+ (1,0) [0|3] ""
368 SG_ Temperature m0 : 8|16@1- (0.1,-40) [-40|125] "°C" *
369 SG_ Pressure m1 : 8|16@1+ (0.01,0) [0|655.35] "kPa" *
370"#,
371        )
372        .unwrap();
373
374        // Encode temperature sensor data (SensorID=0)
375        let payload = dbc.encode(300, &[("SensorID", 0.0), ("Temperature", 25.0)], false).unwrap();
376
377        // Verify SensorID = 0
378        assert_eq!(payload[0], 0x00);
379
380        // Temperature 25°C -> raw = (25 - (-40)) / 0.1 = 650
381        // Little-endian: 650 = 0x028A -> [0x8A, 0x02]
382        assert_eq!(payload[1], 0x8A);
383        assert_eq!(payload[2], 0x02);
384
385        // Decode and verify
386        let decoded = dbc.decode(300, &payload, false).unwrap();
387        let find_value = |name: &str| decoded.iter().find(|s| s.name == name).map(|s| s.value);
388        assert_eq!(find_value("SensorID"), Some(0.0));
389        assert!((find_value("Temperature").unwrap() - 25.0).abs() < 0.1);
390        // Pressure should not be decoded (SensorID != 1)
391        assert!(find_value("Pressure").is_none());
392    }
393
394    #[test]
395    fn test_encode_preserves_unset_bits() {
396        // Test that encoding one signal doesn't affect bits used by other signals
397        let dbc = Dbc::parse(
398            r#"VERSION "1.0"
399
400BU_: ECM
401
402BO_ 256 Engine : 8 ECM
403 SG_ SignalA : 0|8@1+ (1,0) [0|255] ""
404 SG_ SignalB : 8|8@1+ (1,0) [0|255] ""
405"#,
406        )
407        .unwrap();
408
409        // Encode only SignalA
410        let payload = dbc.encode(256, &[("SignalA", 100.0)], false).unwrap();
411        assert_eq!(payload[0], 100);
412        assert_eq!(payload[1], 0); // SignalB should be 0
413
414        // Encode only SignalB
415        let payload = dbc.encode(256, &[("SignalB", 200.0)], false).unwrap();
416        assert_eq!(payload[0], 0); // SignalA should be 0
417        assert_eq!(payload[1], 200);
418
419        // Encode both
420        let payload = dbc.encode(256, &[("SignalA", 100.0), ("SignalB", 200.0)], false).unwrap();
421        assert_eq!(payload[0], 100);
422        assert_eq!(payload[1], 200);
423    }
424
425    #[cfg(feature = "embedded-can")]
426    mod embedded_can_tests {
427        use super::*;
428        use embedded_can::{ExtendedId, Id, StandardId};
429
430        #[test]
431        fn test_encode_for_id_standard() {
432            let dbc = Dbc::parse(
433                r#"VERSION "1.0"
434
435BU_: ECM
436
437BO_ 256 Engine : 8 ECM
438 SG_ RPM : 0|16@1+ (0.25,0) [0|8000] "rpm" *
439"#,
440            )
441            .unwrap();
442
443            let std_id = Id::Standard(StandardId::new(256).unwrap());
444            let payload = dbc.encode_for_id(std_id, &[("RPM", 2000.0)]).unwrap();
445            assert_eq!(payload[0], 0x40);
446            assert_eq!(payload[1], 0x1F);
447        }
448
449        #[test]
450        fn test_encode_for_id_extended() {
451            // 0x80000000 + 0x400 = 2147484672
452            let dbc = Dbc::parse(
453                r#"VERSION "1.0"
454
455BU_: ECM
456
457BO_ 2147484672 ExtendedMsg : 8 ECM
458 SG_ Speed : 0|16@1+ (0.1,0) [0|6553.5] "km/h" *
459"#,
460            )
461            .unwrap();
462
463            let ext_id = Id::Extended(ExtendedId::new(0x400).unwrap());
464            let payload = dbc.encode_for_id(ext_id, &[("Speed", 100.0)]).unwrap();
465            assert_eq!(payload[0], 0xE8);
466            assert_eq!(payload[1], 0x03);
467        }
468    }
469}