Skip to main content

pamoja_can/
signals.rs

1//! The eight data bytes of a J1939 frame, addressed by the signals inside them.
2//!
3//! A parameter group places each signal at a fixed byte offset, little-endian, with a
4//! scale and an offset the standard publishes. Reading or writing one by hand means
5//! slicing the payload and calling `from_le_bytes`, which is where an off-by-one goes
6//! unnoticed. This module does that addressing, and starts a payload filled with the byte
7//! the standard reserves for a signal the sender is not reporting, so a controller only
8//! writes the signals it actually has.
9
10/// The byte a J1939 sender writes for a signal it is not reporting.
11///
12/// A receiver reads this as "not available" rather than as a measurement, which is why a
13/// payload starts filled with it instead of with zeros.
14pub const NOT_AVAILABLE: u8 = 0xFF;
15
16/// The eight data bytes of a J1939 frame.
17///
18/// # Examples
19///
20/// ```
21/// use pamoja_can::Signals;
22///
23/// // Engine speed sits in bytes 4 and 5 of EEC1, at 0.125 rpm per bit.
24/// let mut payload = Signals::new();
25/// payload.set_u16(3, (1000.0 / 0.125) as u16);
26///
27/// assert_eq!(payload.u16(3), Some(8000));
28/// assert_eq!(payload.u8(0), Some(pamoja_can::NOT_AVAILABLE));
29/// ```
30#[derive(Clone, Copy, Debug, PartialEq, Eq)]
31pub struct Signals {
32    bytes: [u8; 8],
33}
34
35impl Default for Signals {
36    fn default() -> Self {
37        Self::new()
38    }
39}
40
41impl Signals {
42    /// Builds a payload with every signal marked not available.
43    ///
44    /// # Returns
45    ///
46    /// Eight bytes of [`NOT_AVAILABLE`], ready for a sender to write only what it has.
47    pub fn new() -> Signals {
48        Signals {
49            bytes: [NOT_AVAILABLE; 8],
50        }
51    }
52
53    /// Reads a payload received off the bus.
54    ///
55    /// # Arguments
56    ///
57    /// * `bytes` - the eight data bytes of a received frame.
58    ///
59    /// # Returns
60    ///
61    /// The payload, ready for its signals to be read out.
62    pub fn from_bytes(bytes: [u8; 8]) -> Signals {
63        Signals { bytes }
64    }
65
66    /// Returns the eight data bytes, ready to put in a frame.
67    ///
68    /// # Returns
69    ///
70    /// The payload in wire order.
71    pub fn as_bytes(&self) -> &[u8; 8] {
72        &self.bytes
73    }
74
75    /// Writes a one-byte signal.
76    ///
77    /// # Arguments
78    ///
79    /// * `at` - the byte offset the parameter group places the signal at, `0..=7`.
80    /// * `value` - the raw value, already scaled as the group defines.
81    ///
82    /// # Returns
83    ///
84    /// The payload, so writes chain.
85    pub fn set_u8(&mut self, at: usize, value: u8) -> &mut Signals {
86        if at < 8 {
87            self.bytes[at] = value;
88        }
89        self
90    }
91
92    /// Writes a two-byte little-endian signal.
93    ///
94    /// # Arguments
95    ///
96    /// * `at` - the offset of the signal's first byte, `0..=6`.
97    /// * `value` - the raw value, already scaled as the group defines.
98    ///
99    /// # Returns
100    ///
101    /// The payload, so writes chain.
102    pub fn set_u16(&mut self, at: usize, value: u16) -> &mut Signals {
103        if at + 1 < 8 {
104            self.bytes[at..at + 2].copy_from_slice(&value.to_le_bytes());
105        }
106        self
107    }
108
109    /// Reads a one-byte signal.
110    ///
111    /// # Arguments
112    ///
113    /// * `at` - the byte offset the parameter group places the signal at.
114    ///
115    /// # Returns
116    ///
117    /// The raw value, or `None` if `at` is past the payload.
118    pub fn u8(&self, at: usize) -> Option<u8> {
119        self.bytes.get(at).copied()
120    }
121
122    /// Reads a two-byte little-endian signal.
123    ///
124    /// # Arguments
125    ///
126    /// * `at` - the offset of the signal's first byte.
127    ///
128    /// # Returns
129    ///
130    /// The raw value, or `None` if the signal would run past the payload.
131    pub fn u16(&self, at: usize) -> Option<u16> {
132        if at + 1 < 8 {
133            Some(u16::from_le_bytes([self.bytes[at], self.bytes[at + 1]]))
134        } else {
135            None
136        }
137    }
138}
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143
144    #[test]
145    fn a_new_payload_reports_nothing() {
146        let payload = Signals::new();
147        assert_eq!(payload.as_bytes(), &[NOT_AVAILABLE; 8]);
148        assert_eq!(payload.u8(0), Some(NOT_AVAILABLE));
149        assert_eq!(payload.u16(0), Some(0xFFFF));
150    }
151
152    #[test]
153    fn a_signal_reads_back_from_where_it_was_written() {
154        let mut payload = Signals::new();
155        payload.set_u16(3, 8_000).set_u8(2, 125);
156
157        assert_eq!(payload.u16(3), Some(8_000));
158        assert_eq!(payload.u8(2), Some(125));
159        assert_eq!(payload.u8(0), Some(NOT_AVAILABLE));
160        assert_eq!(Signals::from_bytes(*payload.as_bytes()), payload);
161    }
162
163    #[test]
164    fn a_signal_past_the_payload_is_refused_rather_than_wrapped() {
165        let mut payload = Signals::new();
166        payload.set_u8(8, 1);
167        payload.set_u16(7, 1);
168
169        assert_eq!(payload.as_bytes(), &[NOT_AVAILABLE; 8]);
170        assert_eq!(payload.u8(8), None);
171        assert_eq!(payload.u16(7), None);
172    }
173}