kcan 0.1.1

CAN controller primitives for actuator and motor control.
Documentation
use std::collections::VecDeque;
use std::time::Duration;

use kcan::{
    CAN_TRACE_CSV_HEADER, CanBus, CanFrame, CanFrameClass, CanId, CanMonitor, CanTraceRecord,
    ControlMode, Error, FormattedCanFrame, Result, parse_can_trace_csv, replay_can_trace,
};

#[test]
fn monitor_keeps_bounded_rolling_records() {
    let mut monitor = CanMonitor::new(2);

    monitor.observe(CanFrame::new(CanId::standard(1).unwrap(), &[1]).unwrap());
    monitor.observe(CanFrame::new(CanId::standard(2).unwrap(), &[2]).unwrap());
    monitor.observe(CanFrame::new(CanId::standard(3).unwrap(), &[3]).unwrap());

    let stats = monitor.stats();
    assert_eq!(stats.frames_seen, 3);
    assert_eq!(stats.frames_retained, 2);
    assert_eq!(stats.frames_dropped, 1);
    assert_eq!(monitor.records().front().unwrap().sequence, 1);
    assert_eq!(monitor.records().back().unwrap().sequence, 2);
}

#[test]
fn classifier_labels_known_protocol_ids() {
    let cubemars = CanFrame::new(
        ControlMode::VelocityLoop.extended_id(0x03).unwrap(),
        &[0, 0, 0, 1],
    )
    .unwrap();
    let robstride = CanFrame::new(CanId::extended(0x501).unwrap(), &[0; 8]).unwrap();

    assert_eq!(
        CanFrameClass::classify(&cubemars),
        CanFrameClass::CubeMarsDirect {
            mode: ControlMode::VelocityLoop,
            motor_id: 0x03
        }
    );
    assert_eq!(
        CanFrameClass::classify(&robstride),
        CanFrameClass::RobStrideStatus { motor_id: 1 }
    );
}

#[test]
fn formatted_frame_includes_hex_and_ascii_data() {
    let frame = CanFrame::new(CanId::standard(0x123).unwrap(), b"A\0z").unwrap();
    let formatted = FormattedCanFrame::from_frame(&frame);

    assert_eq!(formatted.id, "123");
    assert_eq!(formatted.data, "41 00 7A");
    assert_eq!(formatted.ascii, "A.z");
    assert_eq!(formatted.len, 3);
}

#[test]
fn monitor_polls_available_frames_until_timeout() {
    let mut bus = FakeBus::new(vec![
        CanFrame::new(CanId::standard(1).unwrap(), &[1]).unwrap(),
        CanFrame::new(CanId::extended(0x501).unwrap(), &[2]).unwrap(),
    ]);
    let mut monitor = CanMonitor::new(8);

    let received = monitor
        .poll_available(&mut bus, Duration::from_millis(1), 8)
        .unwrap();

    assert_eq!(received, 2);
    assert_eq!(monitor.stats().frames_seen, 2);
    assert_eq!(monitor.stats().standard_frames, 1);
    assert_eq!(monitor.stats().extended_frames, 1);
}

#[test]
fn monitor_exports_and_parses_trace_csv() {
    let mut monitor = CanMonitor::new(4);
    let standard = CanFrame::new(CanId::standard(0x123).unwrap(), b"kcan").unwrap();
    let extended = CanFrame::new(CanId::extended(0x1A_BCDE).unwrap(), &[0xA5, 0x5A]).unwrap();

    monitor.observe(standard.clone());
    monitor.observe(extended.clone());

    let csv = monitor.to_trace_csv();
    assert!(csv.starts_with(CAN_TRACE_CSV_HEADER));
    assert!(csv.contains(",S,123,4,6B63616E"));
    assert!(csv.contains(",E,001ABCDE,2,A55A"));

    let trace = parse_can_trace_csv(&csv).unwrap();
    assert_eq!(trace.len(), 2);
    assert_eq!(trace[0].sequence, 0);
    assert_eq!(trace[0].frame, standard);
    assert_eq!(trace[1].sequence, 1);
    assert_eq!(trace[1].frame, extended);
}

#[test]
fn trace_fixture_parses_expected_protocol_classes() {
    let trace = parse_can_trace_csv(include_str!("fixtures/can_trace_protocols.csv")).unwrap();
    let classes = trace
        .iter()
        .map(|record| CanFrameClass::classify(&record.frame))
        .collect::<Vec<_>>();

    assert_eq!(trace.len(), 4);
    assert_eq!(trace[0].sequence, 0);
    assert_eq!(trace[1].elapsed, Duration::from_micros(1000));
    assert_eq!(trace[3].frame.data(), b"kcan");
    assert_eq!(
        classes,
        vec![
            CanFrameClass::CubeMarsDirect {
                mode: ControlMode::VelocityLoop,
                motor_id: 0x03
            },
            CanFrameClass::CubeMarsMit { motor_id: 0x02 },
            CanFrameClass::RobStrideStatus { motor_id: 0x01 },
            CanFrameClass::Standard { id: 0x123 }
        ]
    );
}

#[test]
fn trace_csv_parser_rejects_bad_payload_length() {
    let err =
        parse_can_trace_csv("sequence,elapsed_us,frame_format,id,len,data_hex\n0,0,S,123,2,AA\n")
            .unwrap_err();

    assert!(
        err.to_string()
            .contains("data_hex length 2 does not match len 2")
    );
}

#[test]
fn trace_csv_parser_rejects_invalid_standard_id() {
    let err =
        parse_can_trace_csv("sequence,elapsed_us,frame_format,id,len,data_hex\n0,0,S,800,0,\n")
            .unwrap_err();

    assert!(
        err.to_string()
            .contains("standard id 0x800 exceeds 11-bit range")
    );
}

#[test]
fn trace_replay_sends_frames_in_order() {
    let first = CanFrame::new(CanId::standard(0x100).unwrap(), &[1]).unwrap();
    let second = CanFrame::new(CanId::extended(0x400).unwrap(), &[2, 3]).unwrap();
    let trace = vec![
        CanTraceRecord {
            sequence: 4,
            elapsed: Duration::from_micros(10),
            frame: first.clone(),
        },
        CanTraceRecord {
            sequence: 5,
            elapsed: Duration::from_micros(20),
            frame: second.clone(),
        },
    ];
    let mut bus = RecordingBus::default();

    let sent = replay_can_trace(&mut bus, &trace).unwrap();

    assert_eq!(sent, 2);
    assert_eq!(bus.sent, vec![first, second]);
}

struct FakeBus {
    frames: VecDeque<CanFrame>,
}

#[derive(Default)]
struct RecordingBus {
    sent: Vec<CanFrame>,
}

impl FakeBus {
    fn new(frames: Vec<CanFrame>) -> Self {
        Self {
            frames: frames.into(),
        }
    }
}

impl CanBus for FakeBus {
    fn send(&mut self, _frame: &CanFrame) -> Result<()> {
        Err(Error::Transport("not implemented".to_owned()))
    }

    fn receive(&mut self, _timeout: Duration) -> Result<Option<CanFrame>> {
        Ok(self.frames.pop_front())
    }
}

impl CanBus for RecordingBus {
    fn send(&mut self, frame: &CanFrame) -> Result<()> {
        self.sent.push(frame.clone());
        Ok(())
    }

    fn receive(&mut self, _timeout: Duration) -> Result<Option<CanFrame>> {
        Ok(None)
    }
}