use crate::canopen::tpdo_config::{SdoWrite, TpdoEntry};
use crate::error::{Error, Result};
#[derive(Debug, Clone)]
pub struct RpdoRecipe {
pub rpdo_index: u8,
pub cob_id: u16,
pub entries: Vec<TpdoEntry>,
pub transmission_type: u8,
}
impl RpdoRecipe {
pub fn validate(&self) -> Result<()> {
if self.rpdo_index > 3 {
return Err(Error::Internal(format!(
"invalid rpdo_index {}, must be 0..=3",
self.rpdo_index
)));
}
if self.entries.is_empty() {
return Err(Error::Internal("RpdoRecipe 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!(
"RpdoRecipe 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)
}
}
const RPDO_COB_DISABLE_BIT: u32 = 0x8000_0000;
pub fn build_rpdo_config_writes(recipe: &RpdoRecipe) -> Result<Vec<SdoWrite>> {
recipe.validate()?;
let idx = recipe.rpdo_index as u16;
let comm_index = 0x1400 + idx;
let map_index = 0x1600 + idx;
let cob_id = recipe.cob_id as u32;
let mut writes = Vec::with_capacity(5 + recipe.entries.len());
writes.push(SdoWrite::u32(comm_index, 1, RPDO_COB_DISABLE_BIT | cob_id));
writes.push(SdoWrite::u8(comm_index, 2, recipe.transmission_type));
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::u32(comm_index, 1, cob_id)); Ok(writes)
}
#[cfg(test)]
mod tests {
use super::*;
fn vel_then_maxtorque() -> Vec<TpdoEntry> {
vec![
TpdoEntry {
index: 0x60FF,
subindex: 0,
bit_len: 32,
},
TpdoEntry {
index: 0x6072,
subindex: 0,
bit_len: 16,
},
]
}
#[test]
fn build_writes_full_sequence() {
let recipe = RpdoRecipe {
rpdo_index: 0,
cob_id: 0x210,
entries: vel_then_maxtorque(),
transmission_type: 255,
};
let w = build_rpdo_config_writes(&recipe).unwrap();
assert_eq!(w.len(), 7);
assert_eq!(w[0], SdoWrite::u32(0x1400, 1, 0x8000_0210)); assert_eq!(w[1], SdoWrite::u8(0x1400, 2, 255)); assert_eq!(w[2], SdoWrite::u8(0x1600, 0, 0)); assert_eq!(w[3], SdoWrite::u32(0x1600, 1, 0x60FF_0020)); assert_eq!(w[4], SdoWrite::u32(0x1600, 2, 0x6072_0010)); assert_eq!(w[5], SdoWrite::u8(0x1600, 0, 2)); assert_eq!(w[6], SdoWrite::u32(0x1400, 1, 0x0000_0210)); }
#[test]
fn total_bytes_round_up() {
let recipe = RpdoRecipe {
rpdo_index: 0,
cob_id: 0x210,
entries: vel_then_maxtorque(),
transmission_type: 255,
};
assert_eq!(recipe.total_bytes(), 6);
}
#[test]
fn validate_bad_rpdo_index() {
let recipe = RpdoRecipe {
rpdo_index: 4,
cob_id: 0x210,
entries: vel_then_maxtorque(),
transmission_type: 255,
};
assert!(recipe.validate().is_err());
}
#[test]
fn validate_bad_cob_id() {
let recipe = RpdoRecipe {
rpdo_index: 0,
cob_id: 0x800,
entries: vel_then_maxtorque(),
transmission_type: 255,
};
assert!(matches!(recipe.validate(), Err(Error::InvalidCobId { .. })));
}
}