dbc_rs/signal/impls.rs
1use super::Signal;
2use crate::{ByteOrder, Receivers};
3use core::hash::{Hash, Hasher};
4
5#[cfg(feature = "std")]
6use crate::MAX_NAME_SIZE;
7#[cfg(feature = "std")]
8use crate::compat::{Comment, String};
9
10impl Signal {
11 /// Number of payload bytes this signal actually spans, accounting for byte order.
12 ///
13 /// Uses the same physical bit-range logic as [`crate::Message::bit_range`], so
14 /// big-endian (Motorola) signals are bounded by their true byte span rather than
15 /// the little-endian forward span `(start_bit + length - 1) / 8`. This is required
16 /// to reject short payloads before indexing in `decode_raw` / `encode_to`.
17 #[inline]
18 pub(crate) fn required_len(&self) -> usize {
19 let (_lsb, msb) = crate::Message::bit_range(self.start_bit, self.length, self.byte_order);
20 (msb as usize / 8) + 1
21 }
22}
23
24impl Signal {
25 #[cfg(feature = "std")]
26 #[allow(clippy::too_many_arguments)] // Internal method, builder pattern is the public API
27 pub(crate) fn new(
28 name: String<{ MAX_NAME_SIZE }>,
29 start_bit: u16,
30 length: u16,
31 byte_order: ByteOrder,
32 unsigned: bool,
33 factor: f64,
34 offset: f64,
35 min: f64,
36 max: f64,
37 unit: Option<String<{ MAX_NAME_SIZE }>>,
38 receivers: Receivers,
39 comment: Option<Comment>,
40 ) -> Self {
41 // Validation should have been done prior (by builder or parse)
42 Self {
43 name,
44 start_bit,
45 length,
46 byte_order,
47 unsigned,
48 factor,
49 offset,
50 min,
51 max,
52 unit,
53 receivers,
54 is_multiplexer_switch: false,
55 multiplexer_switch_value: None,
56 comment,
57 }
58 }
59
60 /// Returns the signal name.
61 ///
62 /// # Examples
63 ///
64 /// ```rust,no_run
65 /// # use dbc_rs::Dbc;
66 /// # let dbc_content = "VERSION \"1.0\"\nBO_ 500 MSG_NAME: 8 ECU\n SG_ SIGNAL_NAME : 0|8@1+ (1,0) [0|0] \"\" ECU\n";
67 /// # let dbc = Dbc::parse(dbc_content).unwrap();
68 /// let message = dbc.messages().find("MSG_NAME").unwrap();
69 /// let signal = message.signals().find("SIGNAL_NAME").unwrap();
70 /// assert_eq!(signal.name(), "SIGNAL_NAME");
71 /// ```
72 #[inline]
73 #[must_use = "return value should be used"]
74 pub fn name(&self) -> &str {
75 self.name.as_ref()
76 }
77
78 /// Returns the start bit position of the signal in the CAN message payload.
79 ///
80 /// The start bit indicates where the signal begins in the message data.
81 /// For little-endian signals, this is the LSB position. For big-endian signals, this is the MSB position.
82 ///
83 /// # Examples
84 ///
85 /// ```rust,no_run
86 /// # use dbc_rs::Dbc;
87 /// # let dbc_content = "VERSION \"1.0\"\nBO_ 500 MSG_NAME: 8 ECU\n SG_ SIGNAL_NAME : 16|8@1+ (1,0) [0|0] \"\" ECU\n";
88 /// # let dbc = Dbc::parse(dbc_content).unwrap();
89 /// let message = dbc.messages().find("MSG_NAME").unwrap();
90 /// let signal = message.signals().find("SIGNAL_NAME").unwrap();
91 /// assert_eq!(signal.start_bit(), 16);
92 /// ```
93 #[inline]
94 #[must_use = "return value should be used"]
95 pub fn start_bit(&self) -> u16 {
96 self.start_bit
97 }
98
99 /// Returns the length of the signal in bits.
100 ///
101 /// # Examples
102 ///
103 /// ```rust,no_run
104 /// # use dbc_rs::Dbc;
105 /// # let dbc_content = "VERSION \"1.0\"\nBO_ 500 MSG_NAME: 8 ECU\n SG_ SIGNAL_NAME : 0|16@1+ (1,0) [0|0] \"\" ECU\n";
106 /// # let dbc = Dbc::parse(dbc_content).unwrap();
107 /// let message = dbc.messages().find("MSG_NAME").unwrap();
108 /// let signal = message.signals().find("SIGNAL_NAME").unwrap();
109 /// assert_eq!(signal.length(), 16);
110 /// ```
111 #[inline]
112 #[must_use = "return value should be used"]
113 pub fn length(&self) -> u16 {
114 self.length
115 }
116
117 /// Returns the byte order (endianness) of the signal.
118 ///
119 /// Returns either [`ByteOrder::LittleEndian`] (Intel format, `@1+`) or [`ByteOrder::BigEndian`] (Motorola format, `@0+`).
120 ///
121 /// # Examples
122 ///
123 /// ```rust,no_run
124 /// # use dbc_rs::{Dbc, ByteOrder};
125 /// # let dbc_content = "VERSION \"1.0\"\nBO_ 500 MSG_NAME: 8 ECU\n SG_ SIGNAL_NAME : 0|8@1+ (1,0) [0|0] \"\" ECU\n";
126 /// # let dbc = Dbc::parse(dbc_content).unwrap();
127 /// let message = dbc.messages().find("MSG_NAME").unwrap();
128 /// let signal = message.signals().find("SIGNAL_NAME").unwrap();
129 /// assert_eq!(signal.byte_order(), ByteOrder::LittleEndian);
130 /// ```
131 #[inline]
132 #[must_use = "return value should be used"]
133 pub fn byte_order(&self) -> ByteOrder {
134 self.byte_order
135 }
136
137 /// Returns `true` if the signal is unsigned, `false` if signed.
138 ///
139 /// In DBC format, `+` indicates unsigned and `-` indicates signed.
140 ///
141 /// # Examples
142 ///
143 /// ```rust,no_run
144 /// # use dbc_rs::Dbc;
145 /// # let dbc_content = "VERSION \"1.0\"\nBO_ 500 MSG_NAME: 8 ECU\n SG_ SIGNAL_NAME : 0|8@1+ (1,0) [0|0] \"\" ECU\n";
146 /// # let dbc = Dbc::parse(dbc_content).unwrap();
147 /// let message = dbc.messages().find("MSG_NAME").unwrap();
148 /// let signal = message.signals().find("SIGNAL_NAME").unwrap();
149 /// assert_eq!(signal.is_unsigned(), true);
150 /// ```
151 #[inline]
152 #[must_use = "return value should be used"]
153 pub fn is_unsigned(&self) -> bool {
154 self.unsigned
155 }
156
157 /// Returns the scaling factor applied to convert raw signal values to physical values.
158 ///
159 /// The physical value is calculated as: `physical_value = raw_value * factor + offset`
160 ///
161 /// # Examples
162 ///
163 /// ```rust,no_run
164 /// # use dbc_rs::Dbc;
165 /// # let dbc_content = "VERSION \"1.0\"\nBO_ 500 MSG_NAME: 8 ECU\n SG_ SIGNAL_NAME : 0|8@1+ (0.5,0) [0|0] \"\" ECU\n";
166 /// # let dbc = Dbc::parse(dbc_content).unwrap();
167 /// let message = dbc.messages().find("MSG_NAME").unwrap();
168 /// let signal = message.signals().find("SIGNAL_NAME").unwrap();
169 /// assert_eq!(signal.factor(), 0.5);
170 /// ```
171 #[inline]
172 #[must_use = "return value should be used"]
173 pub fn factor(&self) -> f64 {
174 self.factor
175 }
176
177 /// Returns the offset applied to convert raw signal values to physical values.
178 ///
179 /// The physical value is calculated as: `physical_value = raw_value * factor + offset`
180 ///
181 /// # Examples
182 ///
183 /// ```rust,no_run
184 /// # use dbc_rs::Dbc;
185 /// # let dbc_content = "VERSION \"1.0\"\nBO_ 500 MSG_NAME: 8 ECU\n SG_ SIGNAL_NAME : 0|8@1+ (1,-40) [0|0] \"\" ECU\n";
186 /// # let dbc = Dbc::parse(dbc_content).unwrap();
187 /// let message = dbc.messages().find("MSG_NAME").unwrap();
188 /// let signal = message.signals().find("SIGNAL_NAME").unwrap();
189 /// assert_eq!(signal.offset(), -40.0);
190 /// ```
191 #[inline]
192 #[must_use = "return value should be used"]
193 pub fn offset(&self) -> f64 {
194 self.offset
195 }
196
197 /// Returns the minimum physical value for this signal.
198 ///
199 /// # Examples
200 ///
201 /// ```rust,no_run
202 /// # use dbc_rs::Dbc;
203 /// # let dbc_content = "VERSION \"1.0\"\nBO_ 500 MSG_NAME: 8 ECU\n SG_ SIGNAL_NAME : 0|8@1+ (1,0) [-40|85] \"\" ECU\n";
204 /// # let dbc = Dbc::parse(dbc_content).unwrap();
205 /// let message = dbc.messages().find("MSG_NAME").unwrap();
206 /// let signal = message.signals().find("SIGNAL_NAME").unwrap();
207 /// assert_eq!(signal.min(), -40.0);
208 /// ```
209 #[inline]
210 #[must_use = "return value should be used"]
211 pub fn min(&self) -> f64 {
212 self.min
213 }
214
215 /// Returns the maximum physical value for this signal.
216 ///
217 /// # Examples
218 ///
219 /// ```rust,no_run
220 /// # use dbc_rs::Dbc;
221 /// # let dbc_content = "VERSION \"1.0\"\nBO_ 500 MSG_NAME: 8 ECU\n SG_ SIGNAL_NAME : 0|8@1+ (1,0) [-40|85] \"\" ECU\n";
222 /// # let dbc = Dbc::parse(dbc_content).unwrap();
223 /// let message = dbc.messages().find("MSG_NAME").unwrap();
224 /// let signal = message.signals().find("SIGNAL_NAME").unwrap();
225 /// assert_eq!(signal.max(), 85.0);
226 /// ```
227 #[inline]
228 #[must_use = "return value should be used"]
229 pub fn max(&self) -> f64 {
230 self.max
231 }
232
233 /// Returns the unit of measurement for this signal, if specified.
234 ///
235 /// Returns `None` if no unit was defined in the DBC file.
236 ///
237 /// # Examples
238 ///
239 /// ```rust,no_run
240 /// # use dbc_rs::Dbc;
241 /// # let dbc_content = "VERSION \"1.0\"\nBO_ 500 MSG_NAME: 8 ECU\n SG_ SIGNAL_NAME : 0|8@1+ (1,0) [0|0] \"km/h\" ECU\n";
242 /// # let dbc = Dbc::parse(dbc_content).unwrap();
243 /// let message = dbc.messages().find("MSG_NAME").unwrap();
244 /// let signal = message.signals().find("SIGNAL_NAME").unwrap();
245 /// assert_eq!(signal.unit(), Some("km/h"));
246 /// ```
247 #[inline]
248 #[must_use = "return value should be used"]
249 pub fn unit(&self) -> Option<&str> {
250 self.unit.as_ref().map(|u| u.as_ref())
251 }
252
253 /// Returns the receivers (ECU nodes) that subscribe to this signal.
254 ///
255 /// Returns a reference to a [`Receivers`] enum which can be either a list of node names or `None`.
256 ///
257 /// # Examples
258 ///
259 /// ```rust,no_run
260 /// # use dbc_rs::Dbc;
261 /// # let dbc_content = "VERSION \"1.0\"\nBO_ 500 MSG_NAME: 8 ECU1\n SG_ SIGNAL_NAME : 0|8@1+ (1,0) [0|0] \"\" ECU2,ECU3\n";
262 /// # let dbc = Dbc::parse(dbc_content).unwrap();
263 /// let message = dbc.messages().find("MSG_NAME").unwrap();
264 /// let signal = message.signals().find("SIGNAL_NAME").unwrap();
265 /// assert_eq!(signal.receivers().len(), 2);
266 /// assert!(signal.receivers().contains("ECU2"));
267 /// ```
268 #[inline]
269 #[must_use = "return value should be used"]
270 pub fn receivers(&self) -> &Receivers {
271 &self.receivers
272 }
273
274 /// Check if this signal is a multiplexer switch (marked with 'M')
275 #[inline]
276 #[must_use = "return value should be used"]
277 pub fn is_multiplexer_switch(&self) -> bool {
278 self.is_multiplexer_switch
279 }
280
281 /// Get the multiplexer switch value if this is a multiplexed signal (marked with 'm0', 'm1', etc.)
282 /// Returns None if this is a normal signal (not multiplexed)
283 #[inline]
284 #[must_use = "return value should be used"]
285 pub fn multiplexer_switch_value(&self) -> Option<u64> {
286 self.multiplexer_switch_value
287 }
288
289 /// Returns the signal comment from CM_ SG_ entry, if present.
290 #[inline]
291 #[must_use = "return value should be used"]
292 pub fn comment(&self) -> Option<&str> {
293 self.comment.as_ref().map(|c| c.as_ref())
294 }
295
296 /// Sets the signal comment (from CM_ SG_ entry).
297 /// Used internally during parsing when CM_ entries are processed after signals.
298 #[inline]
299 pub(crate) fn set_comment(&mut self, comment: crate::compat::Comment) {
300 self.comment = Some(comment);
301 }
302}
303
304impl PartialEq for Signal {
305 fn eq(&self, other: &Self) -> bool {
306 self.name == other.name
307 && self.start_bit == other.start_bit
308 && self.length == other.length
309 && self.byte_order == other.byte_order
310 && self.unsigned == other.unsigned
311 && canonical_f64_bits(self.factor) == canonical_f64_bits(other.factor)
312 && canonical_f64_bits(self.offset) == canonical_f64_bits(other.offset)
313 && canonical_f64_bits(self.min) == canonical_f64_bits(other.min)
314 && canonical_f64_bits(self.max) == canonical_f64_bits(other.max)
315 && self.unit == other.unit
316 && self.receivers == other.receivers
317 && self.is_multiplexer_switch == other.is_multiplexer_switch
318 && self.multiplexer_switch_value == other.multiplexer_switch_value
319 && self.comment == other.comment
320 }
321}
322
323// Custom Eq implementation that handles f64 (treats NaN as equal to NaN, and -0.0 == 0.0)
324impl Eq for Signal {}
325
326// Custom Hash implementation that handles f64 (treats NaN consistently)
327impl Hash for Signal {
328 fn hash<H: Hasher>(&self, state: &mut H) {
329 self.name.hash(state);
330 self.start_bit.hash(state);
331 self.length.hash(state);
332 self.byte_order.hash(state);
333 self.unsigned.hash(state);
334 // Handle f64: convert to bits for hashing (NaN will have consistent representation)
335 canonical_f64_bits(self.factor).hash(state);
336 canonical_f64_bits(self.offset).hash(state);
337 canonical_f64_bits(self.min).hash(state);
338 canonical_f64_bits(self.max).hash(state);
339 self.unit.hash(state);
340 self.receivers.hash(state);
341 self.is_multiplexer_switch.hash(state);
342 self.multiplexer_switch_value.hash(state);
343 self.comment.hash(state);
344 }
345}
346
347#[inline]
348fn canonical_f64_bits(v: f64) -> u64 {
349 // Ensure Hash/Eq are consistent and satisfy the contracts:
350
351 // - Treat -0.0 and 0.0 as equal (and hash identically)
352
353 // - Treat NaN values as equal (and hash identically)
354
355 if v == 0.0 {
356 0.0f64.to_bits()
357 } else if v.is_nan() {
358 f64::NAN.to_bits()
359 } else {
360 v.to_bits()
361 }
362}