use heapless::Vec;
use crate::datatypes::{DataType, Value};
use crate::object_dictionary::Address;
use crate::types::NodeId;
use crate::{Error, Result};
pub mod block;
pub mod client;
pub mod server;
pub use client::{SdoClient, SdoEvent};
pub use server::SdoServer;
pub const SDO_REQUEST_COB_BASE: u16 = 0x600;
pub const SDO_RESPONSE_COB_BASE: u16 = 0x580;
pub type SdoPayload = [u8; 8];
pub const fn request_cob_id(node: NodeId) -> u16 {
SDO_REQUEST_COB_BASE + node.raw() as u16
}
pub const fn response_cob_id(node: NodeId) -> u16 {
SDO_RESPONSE_COB_BASE + node.raw() as u16
}
const CCS_DOWNLOAD_SEGMENT: u8 = 0x00; const CCS_DOWNLOAD_INITIATE: u8 = 0x20; const CCS_UPLOAD_INITIATE: u8 = 0x40; const CCS_UPLOAD_SEGMENT: u8 = 0x60; const SCS_DOWNLOAD_SEGMENT: u8 = 0x20; const SCS_UPLOAD_INITIATE: u8 = 0x40; const SCS_DOWNLOAD_INITIATE: u8 = 0x60; const CS_ABORT: u8 = 0x80;
const CS_MASK: u8 = 0xE0;
const EXPEDITED: u8 = 0x02; const SIZE_INDICATED: u8 = 0x01; const EXPEDITED_SIZED: u8 = EXPEDITED | SIZE_INDICATED; const TOGGLE: u8 = 0x10; const NO_MORE_SEGMENTS: u8 = 0x01;
pub const SEGMENT_DATA_MAX: usize = 7;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum SdoAbortCode {
ToggleBitNotAlternated = 0x0503_0000,
ProtocolTimedOut = 0x0504_0000,
CommandInvalid = 0x0504_0001,
UnsupportedAccess = 0x0601_0000,
ReadOfWriteOnly = 0x0601_0001,
WriteOfReadOnly = 0x0601_0002,
ObjectDoesNotExist = 0x0602_0000,
DataTypeMismatchLengthHigh = 0x0607_0012,
DataTypeMismatchLengthLow = 0x0607_0013,
SubIndexDoesNotExist = 0x0609_0011,
General = 0x0800_0000,
}
pub fn encode_download_expedited(addr: Address, value: &Value) -> Result<SdoPayload> {
let len = value.size();
if len == 0 || len > 4 {
return Err(Error::UnsupportedTransfer);
}
let mut p = [0u8; 8];
p[0] = CCS_DOWNLOAD_INITIATE | (((4 - len) as u8) << 2) | EXPEDITED_SIZED;
p[1..3].copy_from_slice(&addr.index.to_le_bytes());
p[3] = addr.subindex;
value.encode_le(&mut p[4..4 + len])?;
Ok(p)
}
pub fn encode_download_response(addr: Address) -> SdoPayload {
let mut p = [0u8; 8];
p[0] = SCS_DOWNLOAD_INITIATE;
p[1..3].copy_from_slice(&addr.index.to_le_bytes());
p[3] = addr.subindex;
p
}
pub fn decode_download_response(p: &SdoPayload) -> Result<Address> {
if p[0] != SCS_DOWNLOAD_INITIATE {
return Err(Error::UnexpectedCommand);
}
Ok(Address::new(u16::from_le_bytes([p[1], p[2]]), p[3]))
}
pub fn encode_upload_request(addr: Address) -> SdoPayload {
let mut p = [0u8; 8];
p[0] = CCS_UPLOAD_INITIATE;
p[1..3].copy_from_slice(&addr.index.to_le_bytes());
p[3] = addr.subindex;
p
}
pub fn encode_upload_expedited_response(addr: Address, value: &Value) -> Result<SdoPayload> {
let len = value.size();
if len == 0 || len > 4 {
return Err(Error::UnsupportedTransfer);
}
let mut p = [0u8; 8];
p[0] = SCS_UPLOAD_INITIATE | (((4 - len) as u8) << 2) | EXPEDITED_SIZED;
p[1..3].copy_from_slice(&addr.index.to_le_bytes());
p[3] = addr.subindex;
value.encode_le(&mut p[4..4 + len])?;
Ok(p)
}
pub fn decode_upload_expedited_response(
p: &SdoPayload,
data_type: DataType,
) -> Result<(Address, Value)> {
let cmd = p[0];
if cmd & 0xE0 != SCS_UPLOAD_INITIATE || cmd & EXPEDITED_SIZED != EXPEDITED_SIZED {
return Err(Error::UnexpectedCommand);
}
let n = (cmd >> 2) & 0x03;
let len = 4 - n as usize;
if let Some(fixed) = data_type.fixed_size() {
if len != fixed {
return Err(Error::TypeMismatch);
}
}
let addr = Address::new(u16::from_le_bytes([p[1], p[2]]), p[3]);
let value = Value::decode_le(data_type, &p[4..4 + len])?;
Ok((addr, value))
}
pub fn encode_abort(addr: Address, code: SdoAbortCode) -> SdoPayload {
let mut p = [0u8; 8];
p[0] = CS_ABORT;
p[1..3].copy_from_slice(&addr.index.to_le_bytes());
p[3] = addr.subindex;
p[4..8].copy_from_slice(&(code as u32).to_le_bytes());
p
}
pub fn decode_abort(p: &SdoPayload) -> Result<(Address, u32)> {
if p[0] != CS_ABORT {
return Err(Error::UnexpectedCommand);
}
let addr = Address::new(u16::from_le_bytes([p[1], p[2]]), p[3]);
let code = u32::from_le_bytes([p[4], p[5], p[6], p[7]]);
Ok((addr, code))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Segment<'a> {
pub toggle: bool,
pub last: bool,
pub data: &'a [u8],
}
pub fn encode_download_initiate_segmented(addr: Address, size: u32) -> SdoPayload {
let mut p = [0u8; 8];
p[0] = CCS_DOWNLOAD_INITIATE | SIZE_INDICATED;
p[1..3].copy_from_slice(&addr.index.to_le_bytes());
p[3] = addr.subindex;
p[4..8].copy_from_slice(&size.to_le_bytes());
p
}
pub fn decode_download_initiate_segmented(p: &SdoPayload) -> Result<(Address, u32)> {
if p[0] & CS_MASK != CCS_DOWNLOAD_INITIATE
|| p[0] & EXPEDITED != 0
|| p[0] & SIZE_INDICATED == 0
{
return Err(Error::UnexpectedCommand);
}
let addr = Address::new(u16::from_le_bytes([p[1], p[2]]), p[3]);
Ok((addr, u32::from_le_bytes([p[4], p[5], p[6], p[7]])))
}
pub fn encode_upload_initiate_segmented_response(addr: Address, size: u32) -> SdoPayload {
let mut p = [0u8; 8];
p[0] = SCS_UPLOAD_INITIATE | SIZE_INDICATED;
p[1..3].copy_from_slice(&addr.index.to_le_bytes());
p[3] = addr.subindex;
p[4..8].copy_from_slice(&size.to_le_bytes());
p
}
pub fn decode_upload_initiate_segmented_response(p: &SdoPayload) -> Result<(Address, u32)> {
if p[0] & CS_MASK != SCS_UPLOAD_INITIATE || p[0] & EXPEDITED != 0 || p[0] & SIZE_INDICATED == 0
{
return Err(Error::UnexpectedCommand);
}
let addr = Address::new(u16::from_le_bytes([p[1], p[2]]), p[3]);
Ok((addr, u32::from_le_bytes([p[4], p[5], p[6], p[7]])))
}
pub fn encode_data_segment(data: &[u8], toggle: bool, last: bool) -> Result<SdoPayload> {
if data.len() > SEGMENT_DATA_MAX {
return Err(Error::BadLength);
}
let mut p = [0u8; 8];
let n = (SEGMENT_DATA_MAX - data.len()) as u8;
p[0] = (n << 1) & 0x0E;
if toggle {
p[0] |= TOGGLE;
}
if last {
p[0] |= NO_MORE_SEGMENTS;
}
p[1..1 + data.len()].copy_from_slice(data);
Ok(p)
}
pub fn decode_data_segment(p: &SdoPayload) -> Result<Segment<'_>> {
if p[0] & CS_MASK != CCS_DOWNLOAD_SEGMENT {
return Err(Error::UnexpectedCommand);
}
let n = ((p[0] >> 1) & 0x07) as usize;
if n > SEGMENT_DATA_MAX {
return Err(Error::BadLength);
}
Ok(Segment {
toggle: p[0] & TOGGLE != 0,
last: p[0] & NO_MORE_SEGMENTS != 0,
data: &p[1..1 + (SEGMENT_DATA_MAX - n)],
})
}
pub fn encode_download_segment_response(toggle: bool) -> SdoPayload {
let mut p = [0u8; 8];
p[0] = SCS_DOWNLOAD_SEGMENT | if toggle { TOGGLE } else { 0 };
p
}
pub fn decode_download_segment_response(p: &SdoPayload) -> Result<bool> {
if p[0] & CS_MASK != SCS_DOWNLOAD_SEGMENT {
return Err(Error::UnexpectedCommand);
}
Ok(p[0] & TOGGLE != 0)
}
pub fn encode_upload_segment_request(toggle: bool) -> SdoPayload {
let mut p = [0u8; 8];
p[0] = CCS_UPLOAD_SEGMENT | if toggle { TOGGLE } else { 0 };
p
}
pub fn decode_upload_segment_request(p: &SdoPayload) -> Result<bool> {
if p[0] & CS_MASK != CCS_UPLOAD_SEGMENT {
return Err(Error::UnexpectedCommand);
}
Ok(p[0] & TOGGLE != 0)
}
#[derive(Debug)]
pub struct SegmentWriter<'a> {
data: &'a [u8],
pos: usize,
toggle: bool,
}
impl<'a> SegmentWriter<'a> {
pub const fn new(data: &'a [u8]) -> Self {
Self {
data,
pos: 0,
toggle: false,
}
}
pub const fn is_done(&self) -> bool {
self.pos >= self.data.len()
}
pub fn next_segment(&mut self) -> Option<SdoPayload> {
if self.is_done() {
return None;
}
let remaining = self.data.len() - self.pos;
let len = remaining.min(SEGMENT_DATA_MAX);
let last = remaining <= SEGMENT_DATA_MAX;
let frame = encode_data_segment(&self.data[self.pos..self.pos + len], self.toggle, last)
.expect("len is 1..=7");
self.pos += len;
self.toggle = !self.toggle;
Some(frame)
}
}
#[derive(Debug)]
pub struct SegmentReader<const N: usize> {
buf: Vec<u8, N>,
toggle: bool,
done: bool,
}
impl<const N: usize> Default for SegmentReader<N> {
fn default() -> Self {
Self::new()
}
}
impl<const N: usize> SegmentReader<N> {
pub const fn new() -> Self {
Self {
buf: Vec::new(),
toggle: false,
done: false,
}
}
pub const fn is_done(&self) -> bool {
self.done
}
pub fn data(&self) -> &[u8] {
&self.buf
}
pub fn push(&mut self, segment: &Segment) -> Result<()> {
if self.done {
return Err(Error::UnexpectedCommand);
}
if segment.toggle != self.toggle {
return Err(Error::ToggleMismatch);
}
self.buf
.extend_from_slice(segment.data)
.map_err(|_| Error::Overflow)?;
self.toggle = !self.toggle;
if segment.last {
self.done = true;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cob_ids_follow_convention() {
let node = NodeId::new(0x05).unwrap();
assert_eq!(request_cob_id(node), 0x605);
assert_eq!(response_cob_id(node), 0x585);
}
#[test]
fn download_u32_matches_known_frame() {
let f = encode_download_expedited(Address::new(0x2000, 0), &Value::Unsigned32(0x1234_5678))
.unwrap();
assert_eq!(f, [0x23, 0x00, 0x20, 0x00, 0x78, 0x56, 0x34, 0x12]);
}
#[test]
fn download_u8_matches_known_frame() {
let f =
encode_download_expedited(Address::new(0x2001, 0), &Value::Unsigned8(0x7F)).unwrap();
assert_eq!(f, [0x2F, 0x01, 0x20, 0x00, 0x7F, 0x00, 0x00, 0x00]);
}
#[test]
fn download_i16_matches_known_frame() {
let f = encode_download_expedited(Address::new(0x6000, 1), &Value::Integer16(-2)).unwrap();
assert_eq!(f, [0x2B, 0x00, 0x60, 0x01, 0xFE, 0xFF, 0x00, 0x00]);
}
#[test]
fn download_response_roundtrips() {
let f = encode_download_response(Address::new(0x2000, 0));
assert_eq!(f, [0x60, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00]);
assert_eq!(
decode_download_response(&f).unwrap(),
Address::new(0x2000, 0)
);
}
#[test]
fn value_too_large_for_expedited_rejected() {
assert_eq!(
encode_download_expedited(Address::new(0x2000, 0), &Value::Unsigned64(1)),
Err(Error::UnsupportedTransfer)
);
}
#[test]
fn upload_request_matches_known_frame() {
let f = encode_upload_request(Address::new(0x1000, 0));
assert_eq!(f, [0x40, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00]);
}
#[test]
fn upload_response_device_type_decodes() {
let f = [0x43, 0x00, 0x10, 0x00, 0x92, 0x01, 0x00, 0x00];
let (addr, value) = decode_upload_expedited_response(&f, DataType::Unsigned32).unwrap();
assert_eq!(addr, Address::new(0x1000, 0));
assert_eq!(value, Value::Unsigned32(0x0000_0192));
}
#[test]
fn upload_response_encode_matches_known_frame() {
let f = encode_upload_expedited_response(
Address::new(0x1000, 0),
&Value::Unsigned32(0x0000_0192),
)
.unwrap();
assert_eq!(f, [0x43, 0x00, 0x10, 0x00, 0x92, 0x01, 0x00, 0x00]);
}
#[test]
fn upload_response_wrong_type_size_errors() {
let f = [0x43, 0x00, 0x10, 0x00, 0x92, 0x01, 0x00, 0x00];
assert_eq!(
decode_upload_expedited_response(&f, DataType::Unsigned16),
Err(Error::TypeMismatch)
);
}
#[test]
fn decode_upload_rejects_non_upload_frame() {
let f = encode_download_response(Address::new(0x1000, 0));
assert_eq!(
decode_upload_expedited_response(&f, DataType::Unsigned32),
Err(Error::UnexpectedCommand)
);
}
#[test]
fn abort_object_missing_matches_known_frame() {
let f = encode_abort(Address::new(0x1000, 0), SdoAbortCode::ObjectDoesNotExist);
assert_eq!(f, [0x80, 0x00, 0x10, 0x00, 0x00, 0x00, 0x02, 0x06]);
let (addr, code) = decode_abort(&f).unwrap();
assert_eq!(addr, Address::new(0x1000, 0));
assert_eq!(code, 0x0602_0000);
}
#[test]
fn download_initiate_segmented_matches_known_frame() {
let f = encode_download_initiate_segmented(Address::new(0x2000, 0), 20);
assert_eq!(f, [0x21, 0x00, 0x20, 0x00, 20, 0x00, 0x00, 0x00]);
assert_eq!(
decode_download_initiate_segmented(&f).unwrap(),
(Address::new(0x2000, 0), 20)
);
}
#[test]
fn upload_initiate_segmented_response_matches_known_frame() {
let f = encode_upload_initiate_segmented_response(Address::new(0x2000, 0), 20);
assert_eq!(f, [0x41, 0x00, 0x20, 0x00, 20, 0x00, 0x00, 0x00]);
assert_eq!(
decode_upload_initiate_segmented_response(&f).unwrap(),
(Address::new(0x2000, 0), 20)
);
}
#[test]
fn segmented_initiate_rejects_expedited_frame() {
let expedited = [0x43, 0x00, 0x10, 0x00, 0x92, 0x01, 0x00, 0x00];
assert_eq!(
decode_upload_initiate_segmented_response(&expedited),
Err(Error::UnexpectedCommand)
);
}
#[test]
fn data_segment_full_matches_known_frame() {
let f = encode_data_segment(&[1, 2, 3, 4, 5, 6, 7], false, false).unwrap();
assert_eq!(f, [0x00, 1, 2, 3, 4, 5, 6, 7]);
}
#[test]
fn data_segment_last_matches_known_frame() {
let f = encode_data_segment(&[0xAA, 0xBB, 0xCC], true, true).unwrap();
assert_eq!(f, [0x19, 0xAA, 0xBB, 0xCC, 0x00, 0x00, 0x00, 0x00]);
let seg = decode_data_segment(&f).unwrap();
assert!(seg.toggle);
assert!(seg.last);
assert_eq!(seg.data, &[0xAA, 0xBB, 0xCC]);
}
#[test]
fn segment_ack_and_poll_toggle_roundtrip() {
assert_eq!(
encode_download_segment_response(false),
[0x20, 0, 0, 0, 0, 0, 0, 0]
);
assert_eq!(
encode_download_segment_response(true),
[0x30, 0, 0, 0, 0, 0, 0, 0]
);
assert!(decode_download_segment_response(&encode_download_segment_response(true)).unwrap());
assert_eq!(
encode_upload_segment_request(false),
[0x60, 0, 0, 0, 0, 0, 0, 0]
);
assert_eq!(
encode_upload_segment_request(true),
[0x70, 0, 0, 0, 0, 0, 0, 0]
);
assert!(decode_upload_segment_request(&encode_upload_segment_request(true)).unwrap());
}
#[test]
fn segment_writer_reader_roundtrip() {
let payload: [u8; 12] = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120];
let mut writer = SegmentWriter::new(&payload);
let mut reader = SegmentReader::<64>::new();
let f1 = writer.next_segment().unwrap();
assert_eq!(f1[0] & TOGGLE, 0); reader.push(&decode_data_segment(&f1).unwrap()).unwrap();
assert!(!reader.is_done());
let f2 = writer.next_segment().unwrap();
assert_ne!(f2[0] & TOGGLE, 0); reader.push(&decode_data_segment(&f2).unwrap()).unwrap();
assert!(reader.is_done());
assert!(writer.next_segment().is_none());
assert_eq!(reader.data(), &payload);
}
#[test]
fn reader_rejects_toggle_out_of_sequence() {
let mut reader = SegmentReader::<16>::new();
reader
.push(&Segment {
toggle: false,
last: false,
data: &[1, 2, 3],
})
.unwrap();
assert_eq!(
reader.push(&Segment {
toggle: false,
last: true,
data: &[4, 5]
}),
Err(Error::ToggleMismatch)
);
}
#[test]
fn reader_overflow_is_reported() {
let mut reader = SegmentReader::<4>::new();
assert_eq!(
reader.push(&Segment {
toggle: false,
last: false,
data: &[1, 2, 3, 4, 5]
}),
Err(Error::Overflow)
);
}
}