#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u32)]
pub enum SdoAbortCode {
Ok = 0x0000_0000,
ToggleBit = 0x0503_0000,
Timeout = 0x0504_0000,
InvalidCommandSpecifier = 0x0504_0001,
InvalidBlockSize = 0x0504_0002,
ObjectDoesNotExist = 0x0602_0000,
CannotMap = 0x0604_0041,
Incompatibility = 0x0604_0043,
AccessFailed = 0x0606_0000,
SubIndexDoesNotExist = 0x0609_0011,
ValueRangeExceeded = 0x0609_0030,
General = 0x0800_0000,
}
pub type SdoResult<T> = Result<T, SdoAbortCode>;
pub struct SdoClient<const ENTRIES: usize> {
od: [OdEntry; ENTRIES],
n_entries: usize,
}
#[derive(Clone, Copy)]
struct OdEntry {
index: u16,
sub_index: u8,
data: [u8; 4],
byte_len: u8,
read_only: bool,
}
impl SdoClient<64> {
pub fn new() -> Self {
Self {
od: [OdEntry {
index: 0,
sub_index: 0,
data: [0u8; 4],
byte_len: 0,
read_only: false,
}; 64],
n_entries: 0,
}
}
}
impl<const ENTRIES: usize> SdoClient<ENTRIES> {
pub fn define_object(
&mut self,
index: u16,
sub_index: u8,
initial: &[u8],
read_only: bool,
) -> bool {
if self.n_entries >= ENTRIES || initial.len() > 4 {
return false;
}
let mut data = [0u8; 4];
data[..initial.len()].copy_from_slice(initial);
self.od[self.n_entries] = OdEntry {
index,
sub_index,
data,
byte_len: initial.len() as u8,
read_only,
};
self.n_entries += 1;
true
}
fn find_entry(&self, index: u16, sub_index: u8) -> Option<usize> {
self.od[..self.n_entries]
.iter()
.position(|e| e.index == index && e.sub_index == sub_index)
}
pub fn upload(&self, index: u16, sub_index: u8) -> SdoResult<[u8; 4]> {
match self.find_entry(index, sub_index) {
Some(i) => Ok(self.od[i].data),
None => Err(SdoAbortCode::ObjectDoesNotExist),
}
}
pub fn download(&mut self, index: u16, sub_index: u8, data: &[u8]) -> SdoResult<()> {
if data.len() > 4 {
return Err(SdoAbortCode::InvalidBlockSize);
}
match self.find_entry(index, sub_index) {
None => Err(SdoAbortCode::ObjectDoesNotExist),
Some(i) => {
if self.od[i].read_only {
return Err(SdoAbortCode::AccessFailed);
}
let len = data.len().min(self.od[i].byte_len as usize);
self.od[i].data[..len].copy_from_slice(&data[..len]);
Ok(())
}
}
}
pub fn read_u16(&self, index: u16, sub_index: u8) -> SdoResult<u16> {
let data = self.upload(index, sub_index)?;
Ok(u16::from_le_bytes([data[0], data[1]]))
}
pub fn write_u16(&mut self, index: u16, sub_index: u8, val: u16) -> SdoResult<()> {
self.download(index, sub_index, &val.to_le_bytes())
}
pub fn read_i32(&self, index: u16, sub_index: u8) -> SdoResult<i32> {
let data = self.upload(index, sub_index)?;
Ok(i32::from_le_bytes(data))
}
pub fn write_i32(&mut self, index: u16, sub_index: u8, val: i32) -> SdoResult<()> {
self.download(index, sub_index, &val.to_le_bytes())
}
}
impl Default for SdoClient<64> {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sdo_read_write_u16() {
let mut sdo = SdoClient::new();
sdo.define_object(0x6040, 0, &[0x00, 0x00], false);
sdo.write_u16(0x6040, 0, 0x000F).unwrap();
assert_eq!(sdo.read_u16(0x6040, 0).unwrap(), 0x000F);
}
#[test]
fn sdo_read_missing_object() {
let sdo = SdoClient::new();
assert_eq!(
sdo.read_u16(0x1234, 0),
Err(SdoAbortCode::ObjectDoesNotExist)
);
}
#[test]
fn sdo_write_read_only_fails() {
let mut sdo = SdoClient::new();
sdo.define_object(0x1000, 0, &[0x04, 0x00, 0x02, 0x00], true);
assert_eq!(
sdo.write_u16(0x1000, 0, 0x0000),
Err(SdoAbortCode::AccessFailed)
);
assert!(sdo.read_u16(0x1000, 0).is_ok());
}
#[test]
fn sdo_read_write_i32() {
let mut sdo = SdoClient::new();
sdo.define_object(0x607A, 0, &[0; 4], false);
sdo.write_i32(0x607A, 0, -12345).unwrap();
assert_eq!(sdo.read_i32(0x607A, 0).unwrap(), -12345);
}
}