1use 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
23pub struct PamojaCanFrame {
28 frame: Frame,
29}
30
31#[repr(C)]
37#[derive(Clone, Copy, Debug, PartialEq, Eq)]
38pub struct PamojaJ1939Id {
39 pub pgn: u32,
41 pub priority: u8,
43 pub source: u8,
45 pub pdu_format: u8,
47 pub destination: u8,
49 pub addressed: u8,
51}
52
53#[repr(C)]
59#[derive(Clone, Copy, Debug, PartialEq, Eq)]
60pub struct PamojaJ1939Signals {
61 pub bytes: [u8; 8],
63}
64
65pub const PAMOJA_J1939_NOT_AVAILABLE: u8 = 0xFF;
67
68pub const PAMOJA_J1939_BROADCAST_ADDRESS: u8 = 0xFF;
70
71pub const PAMOJA_J1939_PRIORITY_CONTROL: u8 = 3;
73
74pub const PAMOJA_J1939_PRIORITY_DEFAULT: u8 = 6;
76
77pub const PAMOJA_J1939_PRIORITY_LOWEST: u8 = 7;
79
80const _: () = 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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[no_mangle]
332pub extern "C" fn pamoja_can_len_to_dlc(len: usize) -> u8 {
333 len_to_dlc(len)
334}
335
336#[no_mangle]
342pub extern "C" fn pamoja_can_dlc_to_len(dlc: u8) -> usize {
343 dlc_to_len(dlc)
344}
345
346#[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#[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#[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#[no_mangle]
417pub extern "C" fn pamoja_can_signals_new() -> PamojaJ1939Signals {
418 PamojaJ1939Signals {
419 bytes: *Signals::new().as_bytes(),
420 }
421}
422
423#[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#[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#[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#[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
515unsafe 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
550fn identifier(id: u32, extended: bool) -> CanId {
552 if extended {
553 CanId::extended(id)
554 } else {
555 CanId::standard(id as u16)
556 }
557}
558
559unsafe 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
574fn failed(error: CanError) -> PamojaStatus {
576 set_last_error(error.to_string());
577 PamojaStatus::InvalidArgument
578}
579
580fn 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 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 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 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 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 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 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 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 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 let id = pamoja_can_j1939_compose(6, 0x0EA00, 0x21, 0x0A);
715 let mut message = blank();
716 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 let payload = pamoja_can_signals_set_u16(pamoja_can_signals_new(), 3, 8_000);
742 let mut speed = 0u16;
743 assert!(unsafe { pamoja_can_signals_u16(payload, 3, &mut speed) });
745 assert_eq!(speed, 8_000);
746
747 let mut untouched = 0u8;
748 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 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 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}