use crate::error::{Error, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TpdoEntry {
pub index: u16,
pub subindex: u8,
pub bit_len: u8,
}
impl TpdoEntry {
pub fn packed(&self) -> u32 {
((self.index as u32) << 16) | ((self.subindex as u32) << 8) | self.bit_len as u32
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TpdoCommParams {
pub transmission_type: u8,
pub inhibit_time_x100us: u16,
pub event_timer_ms: u16,
}
#[derive(Debug, Clone)]
pub struct TpdoRecipe {
pub tpdo_index: u8,
pub cob_id: u16,
pub entries: Vec<TpdoEntry>,
pub comm: TpdoCommParams,
}
impl TpdoRecipe {
pub fn validate(&self) -> Result<()> {
if self.tpdo_index > 3 {
return Err(Error::Internal(format!(
"invalid tpdo_index {}, must be 0..=3",
self.tpdo_index
)));
}
if self.entries.is_empty() {
return Err(Error::Internal("TpdoRecipe has no entries".into()));
}
if self.entries.len() > 64 {
return Err(Error::Internal(format!(
"too many entries: {}, max 64",
self.entries.len()
)));
}
let total_bits: u32 = self.entries.iter().map(|e| e.bit_len as u32).sum();
if total_bits > 64 * 8 {
return Err(Error::Internal(format!(
"TpdoRecipe total {total_bits} bits > 512 (CAN-FD max payload)"
)));
}
if self.cob_id > 0x7FF {
return Err(Error::InvalidCobId {
cob_id: self.cob_id,
reason: "exceeds 11-bit range",
});
}
Ok(())
}
pub fn total_bytes(&self) -> usize {
self.entries
.iter()
.map(|e| e.bit_len as usize)
.sum::<usize>()
.div_ceil(8)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SdoWrite {
pub index: u16,
pub subindex: u8,
pub data: Vec<u8>,
}
impl SdoWrite {
pub fn u8(index: u16, sub: u8, v: u8) -> Self {
Self {
index,
subindex: sub,
data: vec![v],
}
}
pub fn u16(index: u16, sub: u8, v: u16) -> Self {
Self {
index,
subindex: sub,
data: v.to_le_bytes().to_vec(),
}
}
pub fn u32(index: u16, sub: u8, v: u32) -> Self {
Self {
index,
subindex: sub,
data: v.to_le_bytes().to_vec(),
}
}
pub fn i8(index: u16, sub: u8, v: i8) -> Self {
Self {
index,
subindex: sub,
data: vec![v as u8],
}
}
pub fn i16(index: u16, sub: u8, v: i16) -> Self {
Self {
index,
subindex: sub,
data: v.to_le_bytes().to_vec(),
}
}
pub fn i32(index: u16, sub: u8, v: i32) -> Self {
Self {
index,
subindex: sub,
data: v.to_le_bytes().to_vec(),
}
}
pub fn f32(index: u16, sub: u8, v: f32) -> Self {
Self {
index,
subindex: sub,
data: v.to_le_bytes().to_vec(),
}
}
}
const TPDO_COB_DISABLE_BITS: u32 = 0xC000_0000; const TPDO_COB_ENABLE_BITS: u32 = 0x4000_0000;
pub fn build_tpdo_config_writes(recipe: &TpdoRecipe) -> Result<Vec<SdoWrite>> {
recipe.validate()?;
let idx = recipe.tpdo_index as u16;
let comm_index = 0x1800 + idx;
let map_index = 0x1A00 + idx;
let cob_id = recipe.cob_id as u32;
let mut writes = Vec::with_capacity(7 + recipe.entries.len());
writes.push(SdoWrite::u32(comm_index, 1, TPDO_COB_DISABLE_BITS | cob_id));
writes.push(SdoWrite::u8(map_index, 0, 0));
for (i, entry) in recipe.entries.iter().enumerate() {
writes.push(SdoWrite::u32(map_index, (i + 1) as u8, entry.packed()));
}
writes.push(SdoWrite::u8(map_index, 0, recipe.entries.len() as u8));
writes.push(SdoWrite::u8(comm_index, 2, recipe.comm.transmission_type));
writes.push(SdoWrite::u16(
comm_index,
3,
recipe.comm.inhibit_time_x100us,
));
writes.push(SdoWrite::u16(comm_index, 5, recipe.comm.event_timer_ms));
writes.push(SdoWrite::u32(comm_index, 1, TPDO_COB_ENABLE_BITS | cob_id));
Ok(writes)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn packed_status_word() {
let e = TpdoEntry {
index: 0x6041,
subindex: 0,
bit_len: 16,
};
assert_eq!(e.packed(), 0x6041_0010);
}
#[test]
fn packed_actual_position() {
let e = TpdoEntry {
index: 0x6064,
subindex: 0,
bit_len: 32,
};
assert_eq!(e.packed(), 0x6064_0020);
}
#[test]
fn packed_subindex() {
let e = TpdoEntry {
index: 0x2204,
subindex: 0x02,
bit_len: 16,
};
assert_eq!(e.packed(), 0x2204_0210);
}
#[test]
fn total_bytes_round_up() {
let recipe = TpdoRecipe {
tpdo_index: 0,
cob_id: 0x190,
entries: vec![
TpdoEntry {
index: 0x6041,
subindex: 0,
bit_len: 16,
},
TpdoEntry {
index: 0x6061,
subindex: 0,
bit_len: 8,
},
],
comm: TpdoCommParams {
transmission_type: 255,
inhibit_time_x100us: 5,
event_timer_ms: 20,
},
};
assert_eq!(recipe.total_bytes(), 3);
}
#[test]
fn build_writes_full_sequence() {
let recipe = TpdoRecipe {
tpdo_index: 0,
cob_id: 0x190,
entries: vec![
TpdoEntry {
index: 0x6041,
subindex: 0,
bit_len: 16,
},
TpdoEntry {
index: 0x6064,
subindex: 0,
bit_len: 32,
},
],
comm: TpdoCommParams {
transmission_type: 255,
inhibit_time_x100us: 5,
event_timer_ms: 20,
},
};
let w = build_tpdo_config_writes(&recipe).unwrap();
assert_eq!(w.len(), 9);
assert_eq!(w[0], SdoWrite::u32(0x1800, 1, 0xC000_0190)); assert_eq!(w[1], SdoWrite::u8(0x1A00, 0, 0)); assert_eq!(w[2], SdoWrite::u32(0x1A00, 1, 0x6041_0010)); assert_eq!(w[3], SdoWrite::u32(0x1A00, 2, 0x6064_0020)); assert_eq!(w[4], SdoWrite::u8(0x1A00, 0, 2)); assert_eq!(w[5], SdoWrite::u8(0x1800, 2, 255)); assert_eq!(w[6], SdoWrite::u16(0x1800, 3, 5)); assert_eq!(w[7], SdoWrite::u16(0x1800, 5, 20)); assert_eq!(w[8], SdoWrite::u32(0x1800, 1, 0x4000_0190)); }
#[test]
fn build_writes_tpdo2() {
let recipe = TpdoRecipe {
tpdo_index: 1,
cob_id: 0x290,
entries: vec![TpdoEntry {
index: 0x6041,
subindex: 0,
bit_len: 16,
}],
comm: TpdoCommParams {
transmission_type: 255,
inhibit_time_x100us: 190,
event_timer_ms: 20,
},
};
let w = build_tpdo_config_writes(&recipe).unwrap();
assert_eq!(w.len(), 8);
assert_eq!(w[0].index, 0x1801);
assert_eq!(w[1].index, 0x1A01);
}
#[test]
fn validate_empty_entries() {
let recipe = TpdoRecipe {
tpdo_index: 0,
cob_id: 0x190,
entries: vec![],
comm: TpdoCommParams {
transmission_type: 255,
inhibit_time_x100us: 0,
event_timer_ms: 0,
},
};
assert!(recipe.validate().is_err());
}
#[test]
fn validate_bad_tpdo_index() {
let recipe = TpdoRecipe {
tpdo_index: 4,
cob_id: 0x190,
entries: vec![TpdoEntry {
index: 0x6041,
subindex: 0,
bit_len: 16,
}],
comm: TpdoCommParams {
transmission_type: 255,
inhibit_time_x100us: 0,
event_timer_ms: 0,
},
};
assert!(recipe.validate().is_err());
}
#[test]
fn validate_oversize_payload() {
let recipe = TpdoRecipe {
tpdo_index: 0,
cob_id: 0x190,
entries: (0..9)
.map(|_| TpdoEntry {
index: 0x6041,
subindex: 0,
bit_len: 64,
})
.collect(),
comm: TpdoCommParams {
transmission_type: 255,
inhibit_time_x100us: 0,
event_timer_ms: 0,
},
};
assert!(recipe.validate().is_err());
}
#[test]
fn validate_bad_cob_id() {
let recipe = TpdoRecipe {
tpdo_index: 0,
cob_id: 0x800,
entries: vec![TpdoEntry {
index: 0x6041,
subindex: 0,
bit_len: 16,
}],
comm: TpdoCommParams {
transmission_type: 255,
inhibit_time_x100us: 0,
event_timer_ms: 0,
},
};
assert!(matches!(recipe.validate(), Err(Error::InvalidCobId { .. })));
}
}