Skip to main content

pamoja_ffi/
can.rs

1//! The C ABI for CAN bus framing.
2//!
3//! These functions wrap [`pamoja_can`] for callers that reach the SDK through the
4//! flat C boundary: classic CAN 2.0 and CAN-FD frames, the length encoding CAN-FD
5//! uses above eight bytes, and the J1939 identifier that trucks, tractors, and
6//! gensets ride on top of it.
7//!
8//! A frame carries a payload of up to 64 bytes, so it crosses as an opaque handle
9//! like every other payload-bearing type here. A J1939 identifier is only scalars,
10//! so it crosses by value as [`PamojaJ1939Id`], which keeps decoding an identifier
11//! free of any allocation.
12
13use std::panic::{catch_unwind, AssertUnwindSafe};
14use std::ptr;
15
16use pamoja_can::{
17    dlc_to_len, len_to_dlc, priority, CanError, CanId, Frame, J1939Id, Signals, BROADCAST_ADDRESS,
18    NOT_AVAILABLE,
19};
20
21use crate::{read_bytes, set_last_error, PamojaStatus};
22
23/// An opaque handle to a CAN frame.
24///
25/// Read it with the `pamoja_can_frame_*` calls, then release it with
26/// [`pamoja_can_frame_free`].
27pub struct PamojaCanFrame {
28    frame: Frame,
29}
30
31/// The fields J1939 packs into an extended CAN identifier.
32///
33/// Every field is a scalar, so this crosses the boundary by value. `addressed` is
34/// `1` for a PDU1 message, where `destination` names the node the message is for,
35/// and `0` for a PDU2 broadcast, where `destination` carries no meaning.
36#[repr(C)]
37#[derive(Clone, Copy, Debug, PartialEq, Eq)]
38pub struct PamojaJ1939Id {
39    /// The parameter group number, which names what the message carries.
40    pub pgn: u32,
41    /// The message priority, 0 (highest) to 7.
42    pub priority: u8,
43    /// The source address: the node that sent the message.
44    pub source: u8,
45    /// The PDU format byte of the parameter group.
46    pub pdu_format: u8,
47    /// The destination address, meaningful only when `addressed` is `1`.
48    pub destination: u8,
49    /// `1` for an addressed (PDU1) message, `0` for a broadcast (PDU2) one.
50    pub addressed: u8,
51}
52
53/// The eight data bytes of a J1939 frame, addressed by the signals inside them.
54///
55/// A parameter group places each signal at a fixed byte offset, little-endian. The
56/// payload is only bytes, so it crosses the boundary by value, which keeps reading
57/// and writing a signal free of any allocation.
58#[repr(C)]
59#[derive(Clone, Copy, Debug, PartialEq, Eq)]
60pub struct PamojaJ1939Signals {
61    /// The eight data bytes, in wire order.
62    pub bytes: [u8; 8],
63}
64
65/// The byte a J1939 sender writes for a signal it is not reporting.
66pub const PAMOJA_J1939_NOT_AVAILABLE: u8 = 0xFF;
67
68/// The destination address every node on the bus reads.
69pub const PAMOJA_J1939_BROADCAST_ADDRESS: u8 = 0xFF;
70
71/// The priority a control message takes, ahead of ordinary traffic.
72pub const PAMOJA_J1939_PRIORITY_CONTROL: u8 = 3;
73
74/// The priority ordinary traffic takes.
75pub const PAMOJA_J1939_PRIORITY_DEFAULT: u8 = 6;
76
77/// The priority that yields to everything else on the bus.
78pub const PAMOJA_J1939_PRIORITY_LOWEST: u8 = 7;
79
80// The header generator does not read the crates this one depends on, so these
81// carry their value rather than the name of the constant that defines it.
82const _: () = assert!(PAMOJA_J1939_NOT_AVAILABLE == NOT_AVAILABLE);
83const _: () = assert!(PAMOJA_J1939_BROADCAST_ADDRESS == BROADCAST_ADDRESS);
84const _: () = assert!(PAMOJA_J1939_PRIORITY_CONTROL == priority::CONTROL);
85const _: () = assert!(PAMOJA_J1939_PRIORITY_DEFAULT == priority::DEFAULT);
86const _: () = assert!(PAMOJA_J1939_PRIORITY_LOWEST == priority::LOWEST);
87
88/// Builds a classic CAN 2.0 frame.
89///
90/// # Returns
91///
92/// [`PamojaStatus::Ok`] on success, with `*out_frame` set to a new handle the
93/// caller must release with [`pamoja_can_frame_free`], or
94/// [`PamojaStatus::InvalidArgument`] if the data is longer than the eight bytes a
95/// classic frame carries.
96///
97/// # Safety
98///
99/// `data` must point to at least `data_len` readable bytes, or be null when
100/// `data_len` is 0, and `out_frame` must point to a writable
101/// `*mut PamojaCanFrame`.
102#[no_mangle]
103pub unsafe extern "C" fn pamoja_can_frame_new(
104    id: u32,
105    extended: bool,
106    data: *const u8,
107    data_len: usize,
108    out_frame: *mut *mut PamojaCanFrame,
109) -> PamojaStatus {
110    build(id, extended, data, data_len, out_frame, Frame::new)
111}
112
113/// Builds a CAN-FD frame.
114///
115/// # Returns
116///
117/// [`PamojaStatus::Ok`] on success, with `*out_frame` set to a new handle the
118/// caller must release with [`pamoja_can_frame_free`], or
119/// [`PamojaStatus::InvalidArgument`] if the data is longer than 64 bytes or is
120/// not one of the discrete lengths CAN-FD can carry.
121///
122/// # Safety
123///
124/// `data` must point to at least `data_len` readable bytes, or be null when
125/// `data_len` is 0, and `out_frame` must point to a writable
126/// `*mut PamojaCanFrame`.
127#[no_mangle]
128pub unsafe extern "C" fn pamoja_can_frame_fd(
129    id: u32,
130    extended: bool,
131    data: *const u8,
132    data_len: usize,
133    out_frame: *mut *mut PamojaCanFrame,
134) -> PamojaStatus {
135    build(id, extended, data, data_len, out_frame, Frame::fd)
136}
137
138/// Builds a remote transmission request, which asks another node to send.
139///
140/// # Returns
141///
142/// [`PamojaStatus::Ok`] on success, with `*out_frame` set to a new handle the
143/// caller must release with [`pamoja_can_frame_free`].
144///
145/// # Safety
146///
147/// `out_frame` must point to a writable `*mut PamojaCanFrame`.
148#[no_mangle]
149pub unsafe extern "C" fn pamoja_can_frame_remote(
150    id: u32,
151    extended: bool,
152    len: usize,
153    out_frame: *mut *mut PamojaCanFrame,
154) -> PamojaStatus {
155    let out_frame = match out_slot(out_frame, "out_frame") {
156        Ok(slot) => slot,
157        Err(status) => return status,
158    };
159    match catch_unwind(AssertUnwindSafe(|| {
160        Frame::remote(identifier(id, extended), len)
161    })) {
162        Ok(frame) => {
163            *out_frame = Box::into_raw(Box::new(PamojaCanFrame { frame }));
164            PamojaStatus::Ok
165        }
166        Err(_) => panicked(),
167    }
168}
169
170/// Returns a frame's identifier, already masked to 11 or 29 bits.
171///
172/// # Returns
173///
174/// The identifier, or 0 if `frame` is null.
175///
176/// # Safety
177///
178/// `frame` must be a live handle from one of the frame constructors, or null.
179#[no_mangle]
180pub unsafe extern "C" fn pamoja_can_frame_id(frame: *const PamojaCanFrame) -> u32 {
181    if frame.is_null() {
182        return 0;
183    }
184    (*frame).frame.id().raw()
185}
186
187/// Reports whether a frame carries a 29-bit extended identifier.
188///
189/// # Returns
190///
191/// `true` for an extended identifier, `false` for a standard one or a null frame.
192///
193/// # Safety
194///
195/// `frame` must be a live handle from one of the frame constructors, or null.
196#[no_mangle]
197pub unsafe extern "C" fn pamoja_can_frame_is_extended(frame: *const PamojaCanFrame) -> bool {
198    !frame.is_null() && (*frame).frame.id().is_extended()
199}
200
201/// Reports whether a frame is CAN-FD rather than classic CAN 2.0.
202///
203/// # Returns
204///
205/// `true` for a CAN-FD frame, `false` otherwise or for a null frame.
206///
207/// # Safety
208///
209/// `frame` must be a live handle from one of the frame constructors, or null.
210#[no_mangle]
211pub unsafe extern "C" fn pamoja_can_frame_is_fd(frame: *const PamojaCanFrame) -> bool {
212    !frame.is_null() && (*frame).frame.is_fd()
213}
214
215/// Reports whether a frame is a remote transmission request.
216///
217/// # Returns
218///
219/// `true` for a remote frame, `false` otherwise or for a null frame.
220///
221/// # Safety
222///
223/// `frame` must be a live handle from one of the frame constructors, or null.
224#[no_mangle]
225pub unsafe extern "C" fn pamoja_can_frame_is_remote(frame: *const PamojaCanFrame) -> bool {
226    !frame.is_null() && (*frame).frame.is_remote()
227}
228
229/// Returns how many payload bytes a frame carries.
230///
231/// A remote frame reports the length it requests while carrying no data.
232///
233/// # Returns
234///
235/// The length, or 0 if `frame` is null.
236///
237/// # Safety
238///
239/// `frame` must be a live handle from one of the frame constructors, or null.
240#[no_mangle]
241pub unsafe extern "C" fn pamoja_can_frame_len(frame: *const PamojaCanFrame) -> usize {
242    if frame.is_null() {
243        return 0;
244    }
245    (*frame).frame.len()
246}
247
248/// Returns a frame's data length code, the length as it appears on the wire.
249///
250/// # Returns
251///
252/// The code, or 0 if `frame` is null.
253///
254/// # Safety
255///
256/// `frame` must be a live handle from one of the frame constructors, or null.
257#[no_mangle]
258pub unsafe extern "C" fn pamoja_can_frame_dlc(frame: *const PamojaCanFrame) -> u8 {
259    if frame.is_null() {
260        return 0;
261    }
262    (*frame).frame.dlc()
263}
264
265/// Returns a pointer to a frame's payload bytes.
266///
267/// Use [`pamoja_can_frame_data_len`] for the length, not
268/// [`pamoja_can_frame_len`]: a remote frame reports the length it requests while
269/// carrying no payload at all.
270///
271/// # Returns
272///
273/// A pointer to the payload, or null if `frame` is null or carries no bytes.
274///
275/// # Safety
276///
277/// `frame` must be a live handle from one of the frame constructors, or null.
278#[no_mangle]
279pub unsafe extern "C" fn pamoja_can_frame_data(frame: *const PamojaCanFrame) -> *const u8 {
280    if frame.is_null() {
281        return ptr::null();
282    }
283    let data = (*frame).frame.data();
284    if data.is_empty() {
285        ptr::null()
286    } else {
287        data.as_ptr()
288    }
289}
290
291/// Returns how many bytes [`pamoja_can_frame_data`] points to.
292///
293/// This is the frame's length for an ordinary frame and 0 for a remote one,
294/// which requests a length without carrying the bytes.
295///
296/// # Returns
297///
298/// The payload length, or 0 if `frame` is null.
299///
300/// # Safety
301///
302/// `frame` must be a live handle from one of the frame constructors, or null.
303#[no_mangle]
304pub unsafe extern "C" fn pamoja_can_frame_data_len(frame: *const PamojaCanFrame) -> usize {
305    if frame.is_null() {
306        return 0;
307    }
308    (*frame).frame.data().len()
309}
310
311/// Releases a frame handle.
312///
313/// Passing null is a no-op.
314///
315/// # Safety
316///
317/// `frame` must be a handle from one of the frame constructors that has not
318/// already been freed, or null. After this call it must not be used again.
319#[no_mangle]
320pub unsafe extern "C" fn pamoja_can_frame_free(frame: *mut PamojaCanFrame) {
321    if !frame.is_null() {
322        drop(Box::from_raw(frame));
323    }
324}
325
326/// Returns the data length code that encodes a payload length.
327///
328/// # Returns
329///
330/// The code for `len`, rounding up to the next length CAN-FD can carry.
331#[no_mangle]
332pub extern "C" fn pamoja_can_len_to_dlc(len: usize) -> u8 {
333    len_to_dlc(len)
334}
335
336/// Returns the payload length a data length code encodes.
337///
338/// # Returns
339///
340/// The length in bytes.
341#[no_mangle]
342pub extern "C" fn pamoja_can_dlc_to_len(dlc: u8) -> usize {
343    dlc_to_len(dlc)
344}
345
346/// Decodes the J1939 fields out of an extended CAN identifier.
347///
348/// # Returns
349///
350/// `true` when `extended` is set, with `*out_message` filled in; `false` for a
351/// standard 11-bit identifier, which J1939 does not use, leaving `*out_message`
352/// untouched.
353///
354/// # Safety
355///
356/// `out_message` must point to a writable `PamojaJ1939Id`.
357#[no_mangle]
358pub unsafe extern "C" fn pamoja_can_j1939_decode(
359    id: u32,
360    extended: bool,
361    out_message: *mut PamojaJ1939Id,
362) -> bool {
363    if out_message.is_null() {
364        set_last_error("out_message must not be null".to_owned());
365        return false;
366    }
367    let Some(message) = J1939Id::from_id(identifier(id, extended)) else {
368        return false;
369    };
370    *out_message = PamojaJ1939Id {
371        pgn: message.pgn(),
372        priority: message.priority(),
373        source: message.source(),
374        pdu_format: message.pdu_format(),
375        destination: message.destination().unwrap_or(0),
376        addressed: u8::from(!message.is_broadcast()),
377    };
378    true
379}
380
381/// Composes the extended CAN identifier a set of J1939 fields describes.
382///
383/// # Returns
384///
385/// The 29-bit identifier. `destination` is used only for an addressed (PDU1)
386/// parameter group and ignored for a broadcast (PDU2) one.
387#[no_mangle]
388pub extern "C" fn pamoja_can_j1939_compose(
389    priority: u8,
390    pgn: u32,
391    source: u8,
392    destination: u8,
393) -> u32 {
394    J1939Id::from_parts(priority, pgn, source, destination)
395        .to_id()
396        .raw()
397}
398
399/// Composes the identifier of a J1939 broadcast, which every node on the bus reads.
400///
401/// # Returns
402///
403/// The 29-bit identifier. Most parameter groups are broadcast, so this is the
404/// common case, and it saves a caller knowing the broadcast address.
405#[no_mangle]
406pub extern "C" fn pamoja_can_j1939_broadcast(priority: u8, pgn: u32, source: u8) -> u32 {
407    J1939Id::broadcast(priority, pgn, source).to_id().raw()
408}
409
410/// Builds a J1939 payload with every signal marked not available.
411///
412/// # Returns
413///
414/// Eight bytes of [`PAMOJA_J1939_NOT_AVAILABLE`], ready for a sender to write only
415/// the signals it has.
416#[no_mangle]
417pub extern "C" fn pamoja_can_signals_new() -> PamojaJ1939Signals {
418    PamojaJ1939Signals {
419        bytes: *Signals::new().as_bytes(),
420    }
421}
422
423/// Writes a one-byte signal at the offset its parameter group defines.
424///
425/// # Returns
426///
427/// The payload with the signal written, or unchanged if `at` is past its end.
428#[no_mangle]
429pub extern "C" fn pamoja_can_signals_set_u8(
430    signals: PamojaJ1939Signals,
431    at: usize,
432    value: u8,
433) -> PamojaJ1939Signals {
434    let mut payload = Signals::from_bytes(signals.bytes);
435    payload.set_u8(at, value);
436    PamojaJ1939Signals {
437        bytes: *payload.as_bytes(),
438    }
439}
440
441/// Writes a two-byte little-endian signal at the offset its group defines.
442///
443/// # Returns
444///
445/// The payload with the signal written, or unchanged if the signal would run past
446/// its end.
447#[no_mangle]
448pub extern "C" fn pamoja_can_signals_set_u16(
449    signals: PamojaJ1939Signals,
450    at: usize,
451    value: u16,
452) -> PamojaJ1939Signals {
453    let mut payload = Signals::from_bytes(signals.bytes);
454    payload.set_u16(at, value);
455    PamojaJ1939Signals {
456        bytes: *payload.as_bytes(),
457    }
458}
459
460/// Reads a one-byte signal at the offset its parameter group defines.
461///
462/// # Returns
463///
464/// `true` with `*out_value` set, or `false` if `at` is past the payload.
465///
466/// # Safety
467///
468/// `out_value` must point to a writable `uint8_t`.
469#[no_mangle]
470pub unsafe extern "C" fn pamoja_can_signals_u8(
471    signals: PamojaJ1939Signals,
472    at: usize,
473    out_value: *mut u8,
474) -> bool {
475    if out_value.is_null() {
476        return false;
477    }
478    match Signals::from_bytes(signals.bytes).u8(at) {
479        Some(value) => {
480            ptr::write(out_value, value);
481            true
482        }
483        None => false,
484    }
485}
486
487/// Reads a two-byte little-endian signal at the offset its group defines.
488///
489/// # Returns
490///
491/// `true` with `*out_value` set, or `false` if the signal would run past the
492/// payload.
493///
494/// # Safety
495///
496/// `out_value` must point to a writable `uint16_t`.
497#[no_mangle]
498pub unsafe extern "C" fn pamoja_can_signals_u16(
499    signals: PamojaJ1939Signals,
500    at: usize,
501    out_value: *mut u16,
502) -> bool {
503    if out_value.is_null() {
504        return false;
505    }
506    match Signals::from_bytes(signals.bytes).u16(at) {
507        Some(value) => {
508            ptr::write(out_value, value);
509            true
510        }
511        None => false,
512    }
513}
514
515/// Builds a frame with one of the constructors and hands back a handle.
516///
517/// # Safety
518///
519/// `data` must point to at least `data_len` readable bytes, or be null when
520/// `data_len` is 0, and `out_frame` must point to a writable
521/// `*mut PamojaCanFrame`.
522unsafe fn build(
523    id: u32,
524    extended: bool,
525    data: *const u8,
526    data_len: usize,
527    out_frame: *mut *mut PamojaCanFrame,
528    construct: fn(CanId, &[u8]) -> Result<Frame, CanError>,
529) -> PamojaStatus {
530    let out_frame = match out_slot(out_frame, "out_frame") {
531        Ok(slot) => slot,
532        Err(status) => return status,
533    };
534    let data = match read_bytes(data, data_len) {
535        Ok(data) => data,
536        Err(status) => return status,
537    };
538    match catch_unwind(AssertUnwindSafe(|| {
539        construct(identifier(id, extended), &data)
540    })) {
541        Ok(Ok(frame)) => {
542            *out_frame = Box::into_raw(Box::new(PamojaCanFrame { frame }));
543            PamojaStatus::Ok
544        }
545        Ok(Err(error)) => failed(error),
546        Err(_) => panicked(),
547    }
548}
549
550/// Builds an identifier of the requested width, masking the value to fit it.
551fn identifier(id: u32, extended: bool) -> CanId {
552    if extended {
553        CanId::extended(id)
554    } else {
555        CanId::standard(id as u16)
556    }
557}
558
559/// Rejects a null out-pointer and borrows the slot it names, cleared.
560///
561/// # Safety
562///
563/// `out` must be null or point to a writable `*mut T` that outlives the call.
564unsafe fn out_slot<'a, T>(out: *mut *mut T, name: &str) -> Result<&'a mut *mut T, PamojaStatus> {
565    if out.is_null() {
566        set_last_error(format!("{name} must not be null"));
567        return Err(PamojaStatus::InvalidArgument);
568    }
569    let slot = &mut *out;
570    *slot = ptr::null_mut();
571    Ok(slot)
572}
573
574/// Records a framing error and maps it onto its status.
575fn failed(error: CanError) -> PamojaStatus {
576    set_last_error(error.to_string());
577    PamojaStatus::InvalidArgument
578}
579
580/// Records a caught panic and reports it as [`PamojaStatus::Panic`].
581fn panicked() -> PamojaStatus {
582    set_last_error("panic at the FFI boundary".to_owned());
583    PamojaStatus::Panic
584}
585
586#[cfg(test)]
587mod tests {
588    use super::*;
589
590    /// A zeroed identifier, so a test can tell a written field from an untouched one.
591    fn blank() -> PamojaJ1939Id {
592        PamojaJ1939Id {
593            pgn: 0,
594            priority: 0,
595            source: 0,
596            pdu_format: 0,
597            destination: 0,
598            addressed: 0,
599        }
600    }
601
602    #[test]
603    fn a_classic_frame_carries_its_payload() {
604        let data = [0x01u8, 0xF4];
605        let mut frame = ptr::null_mut();
606
607        // Safety: the input is a valid slice and the out-pointer is writable.
608        unsafe {
609            assert_eq!(
610                pamoja_can_frame_new(0x20A, false, data.as_ptr(), data.len(), &mut frame),
611                PamojaStatus::Ok
612            );
613            assert_eq!(pamoja_can_frame_id(frame), 0x20A);
614            assert!(!pamoja_can_frame_is_extended(frame));
615            assert!(!pamoja_can_frame_is_fd(frame));
616            assert!(!pamoja_can_frame_is_remote(frame));
617            assert_eq!(pamoja_can_frame_len(frame), 2);
618            assert_eq!(pamoja_can_frame_dlc(frame), 2);
619            let payload = std::slice::from_raw_parts(
620                pamoja_can_frame_data(frame),
621                pamoja_can_frame_data_len(frame),
622            );
623            assert_eq!(payload, data);
624            pamoja_can_frame_free(frame);
625        }
626    }
627
628    #[test]
629    fn a_classic_frame_refuses_more_than_eight_bytes() {
630        let data = [0u8; 9];
631        let mut frame = ptr::null_mut();
632        // Safety: the input is a valid slice and the out-pointer is writable.
633        let status =
634            unsafe { pamoja_can_frame_new(0x20A, false, data.as_ptr(), data.len(), &mut frame) };
635        assert_eq!(status, PamojaStatus::InvalidArgument);
636        assert!(frame.is_null());
637    }
638
639    #[test]
640    fn a_fd_frame_takes_a_length_classic_can_cannot() {
641        let data = [0u8; 32];
642        let mut frame = ptr::null_mut();
643
644        // Safety: the input is a valid slice and the out-pointer is writable.
645        unsafe {
646            assert_eq!(
647                pamoja_can_frame_fd(0x1234_5678, true, data.as_ptr(), data.len(), &mut frame),
648                PamojaStatus::Ok
649            );
650            assert!(pamoja_can_frame_is_fd(frame));
651            assert!(pamoja_can_frame_is_extended(frame));
652            assert_eq!(pamoja_can_frame_len(frame), 32);
653            assert_eq!(pamoja_can_frame_dlc(frame), 13);
654            pamoja_can_frame_free(frame);
655        }
656    }
657
658    #[test]
659    fn a_length_between_the_fd_steps_is_refused() {
660        let data = [0u8; 13];
661        let mut frame = ptr::null_mut();
662        // Safety: the input is a valid slice and the out-pointer is writable.
663        let status =
664            unsafe { pamoja_can_frame_fd(0x100, true, data.as_ptr(), data.len(), &mut frame) };
665        assert_eq!(status, PamojaStatus::InvalidArgument);
666        assert!(frame.is_null());
667    }
668
669    #[test]
670    fn a_remote_frame_asks_for_a_length_it_does_not_carry() {
671        let mut frame = ptr::null_mut();
672        // Safety: the out-pointer is writable.
673        unsafe {
674            assert_eq!(
675                pamoja_can_frame_remote(0x20A, false, 4, &mut frame),
676                PamojaStatus::Ok
677            );
678            assert!(pamoja_can_frame_is_remote(frame));
679            assert_eq!(pamoja_can_frame_len(frame), 4, "the length it asks for");
680            assert_eq!(
681                pamoja_can_frame_data_len(frame),
682                0,
683                "a remote frame carries no bytes to read"
684            );
685            assert!(pamoja_can_frame_data(frame).is_null());
686            pamoja_can_frame_free(frame);
687        }
688    }
689
690    #[test]
691    fn an_engine_broadcast_decodes_to_its_parameter_group() {
692        let mut message = blank();
693        // Safety: the out-pointer is writable.
694        let decoded = unsafe { pamoja_can_j1939_decode(0x0CF0_0400, true, &mut message) };
695        assert!(decoded);
696        assert_eq!(message.pgn, 61_444, "electronic engine controller 1");
697        assert_eq!(message.priority, 3);
698        assert_eq!(message.source, 0x00);
699        assert_eq!(message.addressed, 0, "a PDU2 message is a broadcast");
700    }
701
702    #[test]
703    fn a_standard_identifier_is_not_a_j1939_message() {
704        let mut message = blank();
705        // Safety: the out-pointer is writable.
706        let decoded = unsafe { pamoja_can_j1939_decode(0x123, false, &mut message) };
707        assert!(!decoded, "J1939 never rides an 11-bit identifier");
708        assert_eq!(message, blank(), "a refused decode writes nothing");
709    }
710
711    #[test]
712    fn an_addressed_message_round_trips_through_its_identifier() {
713        // A request PGN (0x0EA00) sent from 0x21 to 0x0A at priority 6.
714        let id = pamoja_can_j1939_compose(6, 0x0EA00, 0x21, 0x0A);
715        let mut message = blank();
716        // Safety: the out-pointer is writable.
717        assert!(unsafe { pamoja_can_j1939_decode(id, true, &mut message) });
718        assert_eq!(message.priority, 6);
719        assert_eq!(message.pgn, 0x0EA00);
720        assert_eq!(message.source, 0x21);
721        assert_eq!(message.addressed, 1);
722        assert_eq!(message.destination, 0x0A);
723    }
724
725    #[test]
726    fn the_length_encoding_round_trips() {
727        for len in [0usize, 8, 12, 16, 20, 24, 32, 48, 64] {
728            assert_eq!(pamoja_can_dlc_to_len(pamoja_can_len_to_dlc(len)), len);
729        }
730    }
731
732    #[test]
733    fn a_payload_starts_with_every_signal_not_available() {
734        let payload = pamoja_can_signals_new();
735        assert_eq!(payload.bytes, [PAMOJA_J1939_NOT_AVAILABLE; 8]);
736    }
737
738    #[test]
739    fn a_signal_reads_back_from_where_it_was_written() {
740        // Engine speed sits at byte offset three of EEC1, at 0.125 rpm per bit.
741        let payload = pamoja_can_signals_set_u16(pamoja_can_signals_new(), 3, 8_000);
742        let mut speed = 0u16;
743        // Safety: the out-pointer is writable.
744        assert!(unsafe { pamoja_can_signals_u16(payload, 3, &mut speed) });
745        assert_eq!(speed, 8_000);
746
747        let mut untouched = 0u8;
748        // Safety: the out-pointer is writable.
749        assert!(unsafe { pamoja_can_signals_u8(payload, 0, &mut untouched) });
750        assert_eq!(untouched, PAMOJA_J1939_NOT_AVAILABLE);
751    }
752
753    #[test]
754    fn a_signal_past_the_payload_is_refused_rather_than_wrapped() {
755        let payload = pamoja_can_signals_set_u8(pamoja_can_signals_new(), 8, 1);
756        assert_eq!(payload.bytes, [PAMOJA_J1939_NOT_AVAILABLE; 8]);
757
758        let mut value = 0u16;
759        // Safety: the out-pointer is writable.
760        assert!(!unsafe { pamoja_can_signals_u16(payload, 7, &mut value) });
761    }
762
763    #[test]
764    fn a_broadcast_carries_no_destination() {
765        let id = pamoja_can_j1939_broadcast(PAMOJA_J1939_PRIORITY_CONTROL, 61_444, 0);
766        let mut message = blank();
767        // Safety: the out-pointer is writable.
768        assert!(unsafe { pamoja_can_j1939_decode(id, true, &mut message) });
769        assert_eq!(message.priority, PAMOJA_J1939_PRIORITY_CONTROL);
770        assert_eq!(message.addressed, 0);
771    }
772}