use crate::error::{Error, Result};
use crate::transport::device::{TransportDevice, TransportDeviceInfo};
use crate::transport::transaction::{dispatch_frame, Request};
use moteus_protocol::fdcanusb as codec;
use moteus_protocol::CanFdFrame;
use std::io::{BufRead, BufReader};
use std::time::{Duration, Instant};
pub const DEFAULT_BAUDRATE: u32 = 921600;
pub const DEFAULT_MAX_RETRIES: u32 = 3;
#[non_exhaustive]
#[derive(Debug, Clone)]
pub struct FdcanusbOptions {
pub timeout: Duration,
pub disable_brs: bool,
pub baudrate: u32,
pub uart_mode: Option<bool>,
pub checksum_enabled: bool,
pub max_retries: u32,
}
impl Default for FdcanusbOptions {
fn default() -> Self {
Self::new()
}
}
impl FdcanusbOptions {
pub fn new() -> Self {
Self {
timeout: crate::transport::factory::DEFAULT_TIMEOUT,
disable_brs: false,
baudrate: DEFAULT_BAUDRATE,
uart_mode: None,
checksum_enabled: false,
max_retries: DEFAULT_MAX_RETRIES,
}
}
#[must_use]
pub fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
#[must_use]
pub fn disable_brs(mut self, disable: bool) -> Self {
self.disable_brs = disable;
self
}
#[must_use]
pub fn baudrate(mut self, baudrate: u32) -> Self {
self.baudrate = baudrate;
self
}
#[must_use]
pub fn uart_mode(mut self, uart_mode: bool) -> Self {
self.uart_mode = Some(uart_mode);
self
}
#[must_use]
pub fn checksum_enabled(mut self, enabled: bool) -> Self {
self.checksum_enabled = enabled;
self
}
#[must_use]
pub fn max_retries(mut self, max_retries: u32) -> Self {
self.max_retries = max_retries;
self
}
}
pub struct FdcanusbProtocol;
impl FdcanusbProtocol {
pub fn encode_frame(frame: &CanFdFrame, disable_brs: bool) -> String {
Self::encode_frame_with_options(frame, disable_brs, false)
}
pub fn encode_frame_with_options(
frame: &CanFdFrame,
disable_brs: bool,
checksum: bool,
) -> String {
let mut buf = [0u8; codec::MAX_LINE_LENGTH];
let len = codec::encode_can_send(
frame,
&codec::EncodeOptions {
disable_brs,
checksum,
},
&mut buf,
)
.expect("MAX_LINE_LENGTH is always sufficient");
String::from_utf8_lossy(&buf[..len]).into_owned()
}
pub fn parse_frame(line: &str) -> Option<CanFdFrame> {
codec::parse_rcv(line.as_bytes())
}
pub fn is_ok_response(line: &str) -> bool {
line.trim().starts_with("OK")
}
pub fn is_error_response(line: &str) -> bool {
let trimmed = line.trim();
!trimmed.is_empty() && !trimmed.starts_with("OK") && !trimmed.starts_with("rcv")
}
pub fn is_checksum_error(line: &str) -> bool {
codec::is_checksum_error(line.as_bytes())
}
pub fn is_retryable_error_response(line: &str) -> bool {
let lower = line.to_lowercase();
lower.contains("checksum") || lower.contains("unknown command")
}
}
pub struct FdcanusbDevice<S: std::io::Read + std::io::Write> {
reader: BufReader<S>,
timeout: Duration,
disable_brs: bool,
uart_mode: bool,
checksum_active: bool,
max_retries: u32,
line_buffer: String,
pending_frames: Vec<CanFdFrame>,
needs_flush: bool,
pub(crate) info: TransportDeviceInfo,
}
impl<S: std::io::Read + std::io::Write> std::fmt::Debug for FdcanusbDevice<S> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("FdcanusbDevice")
.field("info", &self.info)
.field("timeout", &self.timeout)
.field("disable_brs", &self.disable_brs)
.field("uart_mode", &self.uart_mode)
.field("checksum_active", &self.checksum_active)
.field("pending_frames", &self.pending_frames.len())
.field("needs_flush", &self.needs_flush)
.finish()
}
}
impl<S: std::io::Read + std::io::Write> FdcanusbDevice<S> {
pub fn from_stream(stream: S) -> Self {
Self::from_stream_with(stream, &FdcanusbOptions::new())
}
pub fn from_stream_with_options(stream: S, timeout: Duration, disable_brs: bool) -> Self {
Self::from_stream_with(
stream,
&FdcanusbOptions::new()
.timeout(timeout)
.disable_brs(disable_brs),
)
}
pub fn from_stream_with(stream: S, options: &FdcanusbOptions) -> Self {
FdcanusbDevice {
reader: BufReader::new(stream),
timeout: options.timeout,
disable_brs: options.disable_brs,
uart_mode: options.uart_mode.unwrap_or(false),
checksum_active: options.checksum_enabled,
max_retries: options.max_retries,
line_buffer: String::new(),
pending_frames: Vec::new(),
needs_flush: false,
info: TransportDeviceInfo::new(0, "Fdcanusb"),
}
}
pub fn set_disable_brs(&mut self, disable: bool) {
self.disable_brs = disable;
}
pub fn uart_mode(&self) -> bool {
self.uart_mode
}
pub fn checksum_active(&self) -> bool {
self.checksum_active
}
fn write_frame(&mut self, frame: &CanFdFrame) -> Result<()> {
let cmd = FdcanusbProtocol::encode_frame_with_options(
frame,
self.disable_brs,
self.checksum_active,
);
self.reader.get_mut().write_all(cmd.as_bytes())?;
Ok(())
}
fn send_frame(&mut self, frame: &CanFdFrame) -> Result<()> {
let mut timeout = self.timeout;
let mut attempt = 0;
loop {
self.write_frame(frame)?;
self.reader.get_mut().flush()?;
match self.wait_for_ok_timeout(timeout) {
Ok(()) => {
if attempt > 0 {
self.needs_flush = true;
}
return Ok(());
}
Err(e)
if self.uart_mode && attempt < self.max_retries && is_retryable_error(&e) =>
{
attempt += 1;
timeout += timeout / 2;
}
Err(e) => {
self.needs_flush = true;
return Err(e);
}
}
}
}
fn read_line_deadline(&mut self, deadline: Instant) -> Result<bool> {
loop {
if Instant::now() > deadline {
return Ok(false);
}
match self.reader.read_line(&mut self.line_buffer) {
Ok(0) => {
return Err(Error::Io(std::io::Error::new(
std::io::ErrorKind::UnexpectedEof,
"Serial port closed",
)));
}
Ok(_) => {
if self.line_buffer.ends_with('\n') {
return Ok(true);
}
}
Err(e)
if e.kind() == std::io::ErrorKind::WouldBlock
|| e.kind() == std::io::ErrorKind::TimedOut =>
{
continue
}
Err(e) => return Err(Error::Io(e)),
}
}
}
fn apply_checksum_policy(&mut self) -> bool {
let (content_len, had_checksum, valid) = {
let trimmed = self.line_buffer.trim_end();
let (content, had_checksum, valid) = codec::strip_checksum(trimmed.as_bytes());
(content.len(), had_checksum, valid)
};
if had_checksum {
if !valid {
return false;
}
self.line_buffer.truncate(content_len);
true
} else {
!self.checksum_active
}
}
fn wait_for_ok(&mut self) -> Result<()> {
self.wait_for_ok_timeout(self.timeout)
}
fn wait_for_ok_timeout(&mut self, timeout: Duration) -> Result<()> {
let deadline = Instant::now() + timeout;
loop {
if !self.read_line_deadline(deadline)? {
return Err(Error::Timeout);
}
if !self.apply_checksum_policy() {
self.line_buffer.clear();
continue;
}
let line = self.line_buffer.trim();
if FdcanusbProtocol::is_ok_response(line) {
self.line_buffer.clear();
return Ok(());
}
if FdcanusbProtocol::is_error_response(line) {
if FdcanusbProtocol::is_checksum_error(line) {
self.checksum_active = true;
}
let error = Error::Device {
message: line.to_string(),
retryable: FdcanusbProtocol::is_retryable_error_response(line),
};
self.line_buffer.clear();
return Err(error);
}
if let Some(frame) = FdcanusbProtocol::parse_frame(&self.line_buffer) {
self.pending_frames.push(frame);
}
self.line_buffer.clear();
}
}
fn receive_frames(&mut self, expected_count: usize) -> Result<Vec<CanFdFrame>> {
let mut frames: Vec<CanFdFrame> = self.pending_frames.drain(..).collect();
if frames.len() >= expected_count {
return Ok(frames);
}
let deadline = Instant::now() + self.timeout;
while frames.len() < expected_count {
if !self.read_line_deadline(deadline)? {
self.needs_flush = true;
break;
}
if !self.apply_checksum_policy() {
self.line_buffer.clear();
continue;
}
if let Some(frame) = FdcanusbProtocol::parse_frame(&self.line_buffer) {
frames.push(frame);
}
self.line_buffer.clear();
}
Ok(frames)
}
}
pub(crate) fn is_retryable_error(error: &Error) -> bool {
match error {
Error::Timeout => true,
Error::Device { retryable, .. } => *retryable,
_ => false,
}
}
impl<S: std::io::Read + std::io::Write> FdcanusbDevice<S> {
fn transaction_pipelined(&mut self, requests: &mut [Request]) -> Result<()> {
let mut frames_sent = 0usize;
for req in requests.iter() {
if let Some(frame) = &req.frame {
self.write_frame(frame)?;
frames_sent += 1;
}
}
if frames_sent > 0 {
self.reader.get_mut().flush()?;
}
for _ in 0..frames_sent {
self.wait_for_ok()?;
}
let expected: usize = Request::total_expected_replies(requests);
if expected > 0 {
let responses = self.receive_frames(expected)?;
for frame in responses {
dispatch_frame(&frame, requests);
}
}
Ok(())
}
fn transaction_uart(&mut self, requests: &mut [Request]) -> Result<()> {
for i in 0..requests.len() {
let Some(frame) = requests[i].frame.clone() else {
continue;
};
self.send_frame(&frame)?;
if requests[i].expected_reply_count > 0 {
self.receive_replies_uart(requests, i)?;
}
}
Ok(())
}
fn receive_replies_uart(&mut self, requests: &mut [Request], target: usize) -> Result<()> {
for frame in std::mem::take(&mut self.pending_frames) {
dispatch_frame(&frame, requests);
}
let expected = requests[target].expected_reply_count as usize;
let deadline = Instant::now() + self.timeout;
while requests[target].responses.len() < expected {
if !self.read_line_deadline(deadline)? {
self.needs_flush = true;
break;
}
if !self.apply_checksum_policy() {
self.line_buffer.clear();
continue;
}
if let Some(frame) = FdcanusbProtocol::parse_frame(&self.line_buffer) {
dispatch_frame(&frame, requests);
}
self.line_buffer.clear();
}
Ok(())
}
fn drain_input(&mut self) {
self.pending_frames.clear();
let deadline = Instant::now() + Duration::from_millis(50);
while let Ok(true) = self.read_line_deadline(deadline) {
self.line_buffer.clear();
}
self.line_buffer.clear();
self.needs_flush = false;
}
fn flush_if_needed(&mut self) {
if self.needs_flush {
self.drain_input();
}
}
}
impl<S: std::io::Read + std::io::Write + Send> TransportDevice for FdcanusbDevice<S> {
fn transaction(&mut self, requests: &mut [Request]) -> Result<()> {
debug_assert!(
requests.iter().all(|r| r.child_device.is_none()),
"FdcanusbDevice does not support child devices"
);
self.flush_if_needed();
let result = if self.uart_mode {
self.transaction_uart(requests)
} else {
self.transaction_pipelined(requests)
};
if result.is_err() {
self.needs_flush = true;
}
result
}
fn write(&mut self, frame: &CanFdFrame) -> Result<()> {
self.flush_if_needed();
self.send_frame(frame)
}
fn read(&mut self) -> Result<Option<CanFdFrame>> {
if let Some(frame) = self.pending_frames.pop() {
return Ok(Some(frame));
}
let deadline = Instant::now() + self.timeout;
loop {
if !self.read_line_deadline(deadline)? {
return Ok(None);
}
if !self.apply_checksum_policy() {
self.line_buffer.clear();
continue;
}
let frame = FdcanusbProtocol::parse_frame(&self.line_buffer);
self.line_buffer.clear();
if frame.is_some() {
return Ok(frame);
}
}
}
fn flush(&mut self) -> Result<()> {
self.drain_input();
Ok(())
}
fn info(&self) -> &TransportDeviceInfo {
&self.info
}
fn set_timeout(&mut self, timeout: Duration) {
self.timeout = timeout;
}
fn timeout(&self) -> Duration {
self.timeout
}
}
#[cfg(feature = "serialport")]
const PORT_READ_TIMEOUT: Duration = Duration::from_millis(10);
#[cfg(feature = "serialport")]
fn open_serial_port(path: &str, baudrate: u32) -> Result<Box<dyn serialport::SerialPort>> {
let mut port = serialport::new(path, baudrate)
.timeout(PORT_READ_TIMEOUT)
.open()
.map_err(|e| Error::Io(e.into()))?;
let _ = port.write_data_terminal_ready(true);
Ok(port)
}
#[cfg(feature = "serialport")]
impl FdcanusbDevice<Box<dyn serialport::SerialPort>> {
pub fn new(path: &str) -> Result<Self> {
Self::open_with(path, &FdcanusbOptions::new())
}
pub fn with_options(path: &str, timeout: Duration, disable_brs: bool) -> Result<Self> {
Self::open_with(
path,
&FdcanusbOptions::new()
.timeout(timeout)
.disable_brs(disable_brs),
)
}
pub fn open_with(path: &str, options: &FdcanusbOptions) -> Result<Self> {
let port = open_serial_port(path, options.baudrate)?;
let mut device = Self::from_stream_with(port, options);
if options.uart_mode.is_none() && !crate::transport::discovery::is_fdcanusb_path(path) {
device.uart_mode = true;
device.checksum_active = true;
}
Ok(device)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::transport::transaction::FrameFilter;
use std::collections::VecDeque;
use std::sync::{Arc, Mutex};
#[derive(Default)]
struct MockSerialInner {
responses: VecDeque<Vec<u8>>,
drop_writes: usize,
read_buffer: Vec<u8>,
written: Vec<u8>,
}
#[derive(Clone, Default)]
struct MockSerial(Arc<Mutex<MockSerialInner>>);
impl MockSerial {
fn queue_response(&self, response: &[u8]) {
self.0
.lock()
.unwrap()
.responses
.push_back(response.to_vec());
}
fn inject(&self, data: &[u8]) {
self.0.lock().unwrap().read_buffer.extend_from_slice(data);
}
fn drop_writes(&self, count: usize) {
self.0.lock().unwrap().drop_writes = count;
}
fn written(&self) -> Vec<u8> {
self.0.lock().unwrap().written.clone()
}
fn command_count(&self) -> usize {
let written = self.written();
written
.windows(b"can send".len())
.filter(|w| w == b"can send")
.count()
}
}
impl std::io::Read for MockSerial {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
let mut inner = self.0.lock().unwrap();
if inner.read_buffer.is_empty() {
return Err(std::io::Error::from(std::io::ErrorKind::TimedOut));
}
let n = buf.len().min(inner.read_buffer.len());
buf[..n].copy_from_slice(&inner.read_buffer[..n]);
inner.read_buffer.drain(..n);
Ok(n)
}
}
impl std::io::Write for MockSerial {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
let mut inner = self.0.lock().unwrap();
inner.written.extend_from_slice(buf);
if inner.drop_writes > 0 {
inner.drop_writes -= 1;
} else if let Some(response) = inner.responses.pop_front() {
inner.read_buffer.extend_from_slice(&response);
}
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
fn test_frame() -> CanFdFrame {
let mut frame = CanFdFrame::new();
frame.arbitration_id = 0x0105;
frame.data[..4].copy_from_slice(b"0123");
frame.size = 4;
frame
}
fn checksummed(content: &str) -> String {
let with_space = format!("{} ", content);
format!(
"{}*{:02X}\n",
with_space,
codec::compute_crc8(with_space.as_bytes())
)
}
fn uart_options() -> FdcanusbOptions {
FdcanusbOptions::new()
.uart_mode(true)
.timeout(Duration::from_millis(10))
}
#[test]
fn test_uart_checksum_send() {
let mock = MockSerial::default();
let mut device =
FdcanusbDevice::from_stream_with(mock.clone(), &uart_options().checksum_enabled(true));
mock.queue_response(b"OK *BD\n");
device.write(&test_frame()).unwrap();
let written = mock.written();
let line = std::str::from_utf8(&written).unwrap().trim_end();
let (content, had, valid) = codec::strip_checksum(line.as_bytes());
assert!(had, "no checksum on: {}", line);
assert!(valid);
assert!(content.starts_with(b"can send 0105 30313233"));
}
#[test]
fn test_uart_checksum_receive() {
let mock = MockSerial::default();
let mut device =
FdcanusbDevice::from_stream_with(mock.clone(), &uart_options().checksum_enabled(true));
let mut response = checksummed("OK");
response.push_str(&checksummed("rcv 0105 2030"));
mock.queue_response(response.as_bytes());
let mut requests = [Request::new(test_frame())
.with_filter(FrameFilter::Any)
.with_expected_replies(1)];
device.transaction(&mut requests).unwrap();
let responses = requests[0].responses.take();
assert_eq!(responses.len(), 1);
assert_eq!(responses[0].payload(), &[0x20, 0x30]);
}
#[test]
fn test_uart_invalid_checksum_discarded() {
let mock = MockSerial::default();
let mut device =
FdcanusbDevice::from_stream_with(mock.clone(), &uart_options().checksum_enabled(true));
let mut response = String::from("OK *FF\n");
response.push_str(&checksummed("OK"));
mock.queue_response(response.as_bytes());
device.write(&test_frame()).unwrap();
assert_eq!(mock.command_count(), 1);
}
#[test]
fn test_uart_missing_checksum_discarded() {
let mock = MockSerial::default();
let mut device = FdcanusbDevice::from_stream_with(
mock.clone(),
&uart_options().checksum_enabled(true).max_retries(0),
);
mock.queue_response(b"OK\n");
assert!(matches!(device.write(&test_frame()), Err(Error::Timeout)));
}
#[test]
fn test_uart_err_checksum_enables_checksums() {
let mock = MockSerial::default();
let mut device = FdcanusbDevice::from_stream_with(mock.clone(), &uart_options());
assert!(!device.checksum_active());
mock.queue_response(b"ERR missing checksum\n");
mock.queue_response(b"OK *BD\n");
device.write(&test_frame()).unwrap();
assert!(device.checksum_active());
let written = mock.written();
let lines: Vec<&str> = std::str::from_utf8(&written)
.unwrap()
.trim_end()
.split('\n')
.collect();
assert_eq!(lines.len(), 2);
assert!(!lines[0].contains('*'), "first send unchecksummed");
let (_, had, valid) = codec::strip_checksum(lines[1].as_bytes());
assert!(had && valid, "retry must carry a checksum");
}
#[test]
fn test_uart_retry_on_timeout() {
let mock = MockSerial::default();
let mut device = FdcanusbDevice::from_stream_with(mock.clone(), &uart_options());
mock.drop_writes(2);
mock.queue_response(b"OK\n");
device.write(&test_frame()).unwrap();
assert_eq!(mock.command_count(), 3);
}
#[test]
fn test_uart_retries_exhausted() {
let mock = MockSerial::default();
let mut device =
FdcanusbDevice::from_stream_with(mock.clone(), &uart_options().max_retries(2));
assert!(matches!(device.write(&test_frame()), Err(Error::Timeout)));
assert_eq!(mock.command_count(), 3); }
#[test]
fn test_fdcanusb_mode_does_not_retry() {
let mock = MockSerial::default();
let mut device = FdcanusbDevice::from_stream_with(
mock.clone(),
&FdcanusbOptions::new()
.uart_mode(false)
.timeout(Duration::from_millis(10)),
);
assert!(matches!(device.write(&test_frame()), Err(Error::Timeout)));
assert_eq!(mock.command_count(), 1);
}
fn source_filter(source: u8) -> FrameFilter {
FrameFilter::custom(move |f| ((f.arbitration_id >> 8) & 0x7F) as u8 == source)
}
#[test]
fn test_uart_stale_reply_flushed_before_next_transaction() {
let mock = MockSerial::default();
let mut device = FdcanusbDevice::from_stream_with(mock.clone(), &uart_options());
mock.queue_response(b"OK\n");
let mut requests = [Request::new(test_frame())
.with_filter(FrameFilter::Any)
.with_expected_replies(1)];
device.transaction(&mut requests).unwrap();
assert!(requests[0].responses.is_empty());
mock.inject(b"rcv 0105 AAAA\n");
mock.queue_response(b"OK\nrcv 0105 BBBB\n");
let mut requests = [Request::new(test_frame())
.with_filter(FrameFilter::Any)
.with_expected_replies(1)];
device.transaction(&mut requests).unwrap();
let responses = requests[0].responses.take();
assert_eq!(responses.len(), 1);
assert_eq!(responses[0].payload(), &[0xBB, 0xBB], "stale reply leaked");
}
#[test]
fn test_uart_reply_counted_per_request_filter() {
let mock = MockSerial::default();
let mut device = FdcanusbDevice::from_stream_with(mock.clone(), &uart_options());
mock.queue_response(b"OK\n");
mock.queue_response(b"OK\nrcv 0100 11\nrcv 0200 22\n");
let mut requests = [
Request::new(test_frame())
.with_filter(source_filter(1))
.with_expected_replies(1),
Request::new(test_frame())
.with_filter(source_filter(2))
.with_expected_replies(1),
];
device.transaction(&mut requests).unwrap();
let r0 = requests[0].responses.take();
let r1 = requests[1].responses.take();
assert_eq!(r0.len(), 1);
assert_eq!(r0[0].payload(), &[0x11]);
assert_eq!(r1.len(), 1);
assert_eq!(r1[0].payload(), &[0x22]);
}
#[test]
fn test_eof_reported_as_error() {
struct EofStream;
impl std::io::Read for EofStream {
fn read(&mut self, _buf: &mut [u8]) -> std::io::Result<usize> {
Ok(0)
}
}
impl std::io::Write for EofStream {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
let mut device = FdcanusbDevice::from_stream_with(
EofStream,
&FdcanusbOptions::new().timeout(Duration::from_millis(10)),
);
assert!(matches!(
device.write(&test_frame()),
Err(Error::Io(e)) if e.kind() == std::io::ErrorKind::UnexpectedEof
));
}
#[test]
fn test_fdcanusb_mode_accepts_checksummed_responses() {
let mock = MockSerial::default();
let mut device = FdcanusbDevice::from_stream_with(
mock.clone(),
&FdcanusbOptions::new().timeout(Duration::from_millis(10)),
);
mock.queue_response(checksummed("OK").as_bytes());
device.write(&test_frame()).unwrap();
}
#[test]
fn test_encode_frame() {
let mut frame = CanFdFrame::new();
frame.arbitration_id = 0x8001;
frame.data[0..3].copy_from_slice(&[0x01, 0x00, 0x0A]);
frame.size = 3;
frame.set_brs(true);
frame.set_fdcan(true);
let encoded = FdcanusbProtocol::encode_frame(&frame, false);
assert!(encoded.starts_with("can send 8001 01000A"));
assert!(encoded.contains("BF"));
}
#[test]
fn test_parse_frame() {
let line = "rcv 8001 01000A0000000000 B F";
let frame = FdcanusbProtocol::parse_frame(line).unwrap();
assert_eq!(frame.arbitration_id, 0x8001);
assert_eq!(frame.data[0], 0x01);
assert_eq!(frame.data[1], 0x00);
assert_eq!(frame.data[2], 0x0A);
assert!(frame.brs_enabled());
assert!(frame.fdcan_enabled());
}
#[test]
fn test_round_up_dlc() {
assert_eq!(CanFdFrame::round_up_dlc(0), 0);
assert_eq!(CanFdFrame::round_up_dlc(8), 8);
assert_eq!(CanFdFrame::round_up_dlc(9), 12);
assert_eq!(CanFdFrame::round_up_dlc(12), 12);
assert_eq!(CanFdFrame::round_up_dlc(13), 16);
assert_eq!(CanFdFrame::round_up_dlc(33), 48);
assert_eq!(CanFdFrame::round_up_dlc(49), 64);
}
#[test]
fn test_is_ok_response() {
assert!(FdcanusbProtocol::is_ok_response("OK\n"));
assert!(FdcanusbProtocol::is_ok_response("OK"));
assert!(FdcanusbProtocol::is_ok_response(" OK "));
assert!(!FdcanusbProtocol::is_ok_response("rcv 8001 00"));
}
}