///|
/// CoE/SDO transaction engine: encode/decode mailbox + CoE frames,
/// and drive expedited SDO Upload/Download transactions.
///
/// Mailbox frame layout (ETG.1000.6):
///   Bytes 0-1: Length (data area only, little-endian)
///   Bytes 2-3: Address (station address, little-endian)
///   Byte 4: bits [7:6]=Priority, [5:2]=reserved, [1:0]=Channel
///   Byte 5: bit [7]=reserved, [6:4]=Counter, [3:0]=Type
///   Bytes 6+: Protocol-specific data
///
/// CoE header (2 bytes):
///   Bits [15:12] = CoE service type
///   Bits [11:0]  = reserved/number
///
/// SDO header (expedited, 8 bytes):
///   Byte 0: Command specifier + size indicator
///   Byte 1-2: Index (little-endian)
///   Byte 3: Subindex
///   Bytes 4-7: Data (expedited) or size (normal)

///|
/// Mailbox header size in bytes.
let mailbox_header_len : Int = 6

///|
/// CoE header size in bytes.
let coe_header_len : Int = 2

///|
/// SDO expedited header size in bytes (command + index + subindex + 4 data).
let sdo_header_len : Int = 8

///|
/// SDO Info header size in bytes.
let sdo_info_header_len : Int = 4

///|
/// Master-originated mailbox requests use address 0x0000 in the header.
fn mailbox_request_address(_station : UInt) -> UInt {
  0U
}

///|
/// Encode a mailbox header into 6 bytes.
pub fn MailboxHeader::to_bytes(self : MailboxHeader) -> Bytes {
  let buf = Buffer(size_hint=mailbox_header_len)
  // Bytes 0-1: Length (LE)
  buf.write_byte((self.length & 0xFFU).to_byte())
  buf.write_byte(((self.length >> 8) & 0xFFU).to_byte())
  // Bytes 2-3: Address (LE)
  buf.write_byte((self.address & 0xFFU).to_byte())
  buf.write_byte(((self.address >> 8) & 0xFFU).to_byte())
  // Byte 4: Channel[1:0] | Priority[7:6]
  let b4 = (self.channel & b'\x03') | ((self.priority & b'\x03') << 6)
  buf.write_byte(b4)
  // Byte 5: Type[3:0] | Counter[6:4], bit 7 remains reserved.
  let b5 = self.mb_type.to_byte() | ((self.counter & b'\x07') << 4)
  buf.write_byte(b5)
  buf.to_bytes()
}

///|
/// Decode a mailbox header from bytes (needs at least 6 bytes).
pub fn MailboxHeader::from_bytes(
  data : Bytes,
) -> MailboxHeader raise @hal.EcError {
  if data.length() < mailbox_header_len {
    raise @hal.EcError::MailboxProtocol("mailbox header too short")
  }
  let length = data[0].to_uint() | (data[1].to_uint() << 8)
  let address = data[2].to_uint() | (data[3].to_uint() << 8)
  let channel = data[4] & b'\x03'
  let priority = (data[4] >> 6) & b'\x03'
  let type_byte = data[5] & b'\x0F'
  let counter = (data[5] >> 4) & b'\x07'
  let mb_type : MailboxType = match type_byte {
    b'\x00' => Err
    b'\x01' => AoE
    b'\x02' => EoE
    b'\x03' => CoE
    b'\x04' => FoE
    b'\x05' => SoE
    b'\x0F' => VoE
    _ =>
      raise @hal.EcError::MailboxProtocol(
        "unknown mailbox type: " + type_byte.to_int().to_string(),
      )
  }
  { length, address, channel, priority, mb_type, counter }
}

///|
/// CoE service type encoding for the CoE header.
fn CoeService::to_uint(self : CoeService) -> UInt {
  match self {
    SdoRequest => 2U
    SdoResponse => 3U
    SdoInfo => 8U
    TxPdo => 5U
    RxPdo => 6U
    PdoRemoteRequest => 7U
    Emergency => 1U
  }
}

///|
/// Decode CoE service type from the 4-bit field.
fn coe_service_from_uint(v : UInt) -> CoeService raise @hal.EcError {
  match v {
    1U => Emergency
    2U => SdoRequest
    3U => SdoResponse
    5U => TxPdo
    6U => RxPdo
    7U => PdoRemoteRequest
    8U => SdoInfo
    _ =>
      raise @hal.EcError::MailboxProtocol(
        "unknown CoE service: " + v.reinterpret_as_int().to_string(),
      )
  }
}

///|
/// SDO command specifier byte encoding.
fn SdoCommand::to_byte(self : SdoCommand) -> Byte {
  match self {
    DownloadRequest => b'\x21' // initiate download, expedited, size=4
    DownloadResponse => b'\x60'
    UploadRequest => b'\x40'
    UploadResponse => b'\x43' // initiate upload response, expedited, size=4
    AbortTransfer => b'\x80'
  }
}

///|
/// Decode SDO command specifier from byte.
fn sdo_command_from_byte(b : Byte) -> SdoCommand raise @hal.EcError {
  // Check command specifier bits [7:5]
  let cs = (b >> 5) & b'\x07'
  match cs {
    b'\x01' => DownloadRequest // 001 = initiate download
    b'\x02' => UploadResponse // 010 = initiate upload response
    b'\x03' => DownloadResponse // 011 = initiate download response
    b'\x04' => AbortTransfer // 100 = abort
    _ =>
      if b == b'\x40' {
        UploadRequest // 010 with no data bits = upload request
      } else {
        raise @hal.EcError::MailboxProtocol(
          "unknown SDO command: 0x" + b.to_int().to_string(),
        )
      }
  }
}

///|
/// SdoTransaction tracks one SDO request-response exchange.
pub(all) struct SdoTransaction {
  /// Mailbox counter for correlation.
  counter : Byte
  /// Target slave station address.
  station : UInt
  /// SDO request to send.
  request : SdoRequest
} derive(Eq, Debug)

///|
/// SdoResponse holds a decoded SDO response.
pub(all) struct SdoResponse {
  index : UInt
  subindex : Byte
  command : SdoCommand
  data : Bytes
  /// True when this response is part of segmented transfer.
  segmented : Bool
  /// Toggle bit used by segmented transfer.
  toggle : Bool
  /// True when this is the last segmented frame.
  last_segment : Bool
  /// Total transfer size if provided by initiate upload response.
  complete_size : UInt?
} derive(Eq, Debug)

///|
/// Build a complete mailbox frame for an SDO request (expedited).
pub fn SdoTransaction::encode(self : SdoTransaction) -> Bytes {
  let buf = Buffer(
    size_hint=mailbox_header_len + coe_header_len + sdo_header_len,
  )
  let data_len = coe_header_len + sdo_header_len
  // Mailbox header
  let mbh : MailboxHeader = {
    length: data_len.reinterpret_as_uint(),
    address: mailbox_request_address(self.station),
    channel: b'\x00',
    priority: b'\x00',
    mb_type: CoE,
    counter: self.counter,
  }
  let hdr_bytes = mbh.to_bytes()
  for i = 0; i < hdr_bytes.length(); i = i + 1 {
    buf.write_byte(hdr_bytes[i])
  }
  // CoE header: service type in bits [15:12]
  let svc = self.request.command.to_coe_service()
  let coe_word = svc.to_uint() << 12
  buf.write_byte((coe_word & 0xFFU).to_byte())
  buf.write_byte(((coe_word >> 8) & 0xFFU).to_byte())
  // SDO header
  let mut cmd = self.request.command.to_byte()
  // Complete Access is bit4 in initiate upload/download command.
  if self.request.complete_access {
    cmd = cmd | b'\x10'
  }
  buf.write_byte(cmd)
  // Index (LE)
  buf.write_byte((self.request.index & 0xFFU).to_byte())
  buf.write_byte(((self.request.index >> 8) & 0xFFU).to_byte())
  // Subindex
  buf.write_byte(self.request.subindex)
  // Data (pad to 4 bytes)
  for i = 0; i < 4; i = i + 1 {
    if i < self.request.data.length() {
      buf.write_byte(self.request.data[i])
    } else {
      buf.write_byte(b'\x00')
    }
  }
  buf.to_bytes()
}

///|
/// Map SDO command to its CoE service type.
fn SdoCommand::to_coe_service(self : SdoCommand) -> CoeService {
  match self {
    DownloadRequest | UploadRequest | AbortTransfer => SdoRequest
    DownloadResponse | UploadResponse => SdoResponse
  }
}

///|
/// Decode an SDO response from a raw mailbox frame.
pub fn decode_sdo_response(
  data : Bytes,
) -> (MailboxHeader, SdoResponse) raise @hal.EcError {
  if data.length() < mailbox_header_len + coe_header_len + 1 {
    raise @hal.EcError::MailboxProtocol("SDO response frame too short")
  }
  let mbh = MailboxHeader::from_bytes(data)
  let payload_len = mbh.length.reinterpret_as_int()
  if payload_len < coe_header_len + 1 {
    raise @hal.EcError::MailboxProtocol("SDO response payload too short")
  }
  let frame_len = mailbox_header_len + payload_len
  if data.length() < frame_len {
    raise @hal.EcError::MailboxProtocol("SDO response truncated")
  }
  // Verify it's a CoE mailbox
  if mbh.mb_type != CoE {
    raise @hal.EcError::MailboxProtocol(
      "expected CoE mailbox, got " + @debug.to_string(mbh.mb_type),
    )
  }
  let off = mailbox_header_len
  let sdo_payload_len = payload_len - coe_header_len
  // CoE header
  let coe_word = data[off].to_uint() | (data[off + 1].to_uint() << 8)
  let svc_field = (coe_word >> 12) & 0xFU
  let _svc = coe_service_from_uint(svc_field)
  // SDO header
  let sdo_off = off + coe_header_len
  let cmd_byte = data[sdo_off]

  // Abort transfer response
  if cmd_byte == b'\x80' {
    if sdo_payload_len < 8 {
      raise @hal.EcError::MailboxProtocol("SDO abort payload too short")
    }
    let abort_off = sdo_off + 4
    let abort_code = data[abort_off].to_uint() |
      (data[abort_off + 1].to_uint() << 8) |
      (data[abort_off + 2].to_uint() << 16) |
      (data[abort_off + 3].to_uint() << 24)
    raise @hal.EcError::SdoAbort(abort_code)
  }

  // Segmented upload response: command specifier in bits[7:5] = 000
  if (cmd_byte & b'\xE0') == b'\x00' {
    let toggle = (cmd_byte & b'\x10') != b'\x00'
    let n_unused = ((cmd_byte >> 1) & b'\x07').to_int()
    let last = (cmd_byte & b'\x01') != b'\x00'
    let payload_after_cmd = sdo_payload_len - 1
    let seg_len = if payload_after_cmd > 7 - n_unused {
      7 - n_unused
    } else {
      payload_after_cmd
    }
    let seg_data = if seg_len > 0 {
      Bytes::makei(seg_len, fn(i) { data[sdo_off + 1 + i] })
    } else {
      Bytes::new(0)
    }
    let resp : SdoResponse = {
      index: 0U,
      subindex: b'\x00',
      command: UploadResponse,
      data: seg_data,
      segmented: true,
      toggle,
      last_segment: last,
      complete_size: None,
    }
    return (mbh, resp)
  }

  // Segmented download response: command specifier in bits[7:5] = 001,
  // and payload contains only command byte (no index/subindex fields).
  if (cmd_byte & b'\xE0') == b'\x20' && sdo_payload_len == 1 {
    let toggle = (cmd_byte & b'\x10') != b'\x00'
    let resp : SdoResponse = {
      index: 0U,
      subindex: b'\x00',
      command: DownloadResponse,
      data: Bytes::new(0),
      segmented: true,
      toggle,
      last_segment: true,
      complete_size: None,
    }
    return (mbh, resp)
  }

  let cmd = sdo_command_from_byte(cmd_byte)

  if sdo_payload_len < sdo_header_len {
    raise @hal.EcError::MailboxProtocol("SDO response header too short")
  }
  let index = data[sdo_off + 1].to_uint() | (data[sdo_off + 2].to_uint() << 8)
  let subindex = data[sdo_off + 3]

  // Initiate upload response: command specifier bits[7:5] = 010
  if cmd == UploadResponse {
    let expedited = (cmd_byte & b'\x02') != b'\x00'
    let size_indicated = (cmd_byte & b'\x01') != b'\x00'
    if expedited {
      let n_unused = ((cmd_byte >> 2) & b'\x03').to_int()
      let data_len = 4 - n_unused
      let resp_data = Bytes::makei(data_len, fn(i) { data[sdo_off + 4 + i] })
      let resp : SdoResponse = {
        index,
        subindex,
        command: UploadResponse,
        data: resp_data,
        segmented: false,
        toggle: false,
        last_segment: true,
        complete_size: None,
      }
      return (mbh, resp)
    }

    let complete_size = if size_indicated {
      Some(
        data[sdo_off + 4].to_uint() |
        (data[sdo_off + 5].to_uint() << 8) |
        (data[sdo_off + 6].to_uint() << 16) |
        (data[sdo_off + 7].to_uint() << 24),
      )
    } else {
      None
    }
    let extra_len = sdo_payload_len - sdo_header_len
    let extra_data = if extra_len > 0 {
      Bytes::makei(extra_len, fn(i) { data[sdo_off + sdo_header_len + i] })
    } else {
      Bytes::new(0)
    }
    let resp : SdoResponse = {
      index,
      subindex,
      command: UploadResponse,
      data: extra_data,
      segmented: true,
      toggle: false,
      last_segment: false,
      complete_size,
    }
    return (mbh, resp)
  }

  let sdo_data = Bytes::makei(4, fn(i) { data[sdo_off + 4 + i] })
  let resp : SdoResponse = {
    index,
    subindex,
    command: cmd,
    data: sdo_data,
    segmented: false,
    toggle: false,
    last_segment: true,
    complete_size: None,
  }
  (mbh, resp)
}

///|
/// MailboxCounter manages the mailbox session counter cycling (1-7, wrapping).
pub(all) struct MailboxCounter {
  mut value : Byte
} derive(Debug)

///|
pub fn MailboxCounter::new() -> MailboxCounter {
  { value: b'\x01' }
}

///|
/// Get the current counter value and advance.
pub fn MailboxCounter::next(self : MailboxCounter) -> Byte {
  let current = self.value
  let next_val = self.value.to_int() % 7 + 1
  self.value = next_val.to_byte()
  current
}

///|
/// Build an SDO Download (write) transaction.
pub fn build_sdo_download(
  counter : Byte,
  station : UInt,
  index : UInt,
  subindex : Byte,
  data : Bytes,
) -> SdoTransaction {
  {
    counter,
    station,
    request: {
      index,
      subindex,
      command: DownloadRequest,
      data,
      complete_access: false,
    },
  }
}

///|
/// Build an SDO Upload (read) transaction.
pub fn build_sdo_upload(
  counter : Byte,
  station : UInt,
  index : UInt,
  subindex : Byte,
) -> SdoTransaction {
  {
    counter,
    station,
    request: {
      index,
      subindex,
      command: UploadRequest,
      data: Bytes::new(0),
      complete_access: false,
    },
  }
}

///|
/// Build an SDO Download transaction with Complete Access flag set.
pub fn build_sdo_download_ca(
  counter : Byte,
  station : UInt,
  index : UInt,
  subindex : Byte,
  data : Bytes,
) -> SdoTransaction {
  {
    counter,
    station,
    request: {
      index,
      subindex,
      command: DownloadRequest,
      data,
      complete_access: true,
    },
  }
}

///|
/// Build an SDO Upload transaction with Complete Access flag set.
pub fn build_sdo_upload_ca(
  counter : Byte,
  station : UInt,
  index : UInt,
  subindex : Byte,
) -> SdoTransaction {
  {
    counter,
    station,
    request: {
      index,
      subindex,
      command: UploadRequest,
      data: Bytes::new(0),
      complete_access: true,
    },
  }
}

///|
/// Build an SDO Upload Segment request frame.
/// Toggle bit alternates on each segment round-trip.
pub fn build_sdo_upload_segment_request(
  counter : Byte,
  station : UInt,
  toggle : Bool,
) -> Bytes {
  let buf = Buffer(
    size_hint=mailbox_header_len + coe_header_len + sdo_header_len,
  )
  let data_len = coe_header_len + sdo_header_len
  let mbh : MailboxHeader = {
    length: data_len.reinterpret_as_uint(),
    address: mailbox_request_address(station),
    channel: b'\x00',
    priority: b'\x00',
    mb_type: CoE,
    counter,
  }
  let hdr_bytes = mbh.to_bytes()
  for i in 0..> 8) & 0xFFU).to_byte())
  let cmd = if toggle { b'\x70' } else { b'\x60' }
  buf.write_byte(cmd)
  for _i in 0..<7 {
    buf.write_byte(b'\x00')
  }
  buf.to_bytes()
}

///|
/// Build an SDO Download Segment request frame.
/// `segment` may contain at most 7 bytes.
pub fn build_sdo_download_segment_request(
  counter : Byte,
  station : UInt,
  toggle : Bool,
  last_segment : Bool,
  segment : Bytes,
) -> Bytes raise @hal.EcError {
  if segment.length() > 7 {
    raise @hal.EcError::MailboxProtocol(
      "SDO download segment too large (>7 bytes)",
    )
  }
  let buf = Buffer(
    size_hint=mailbox_header_len + coe_header_len + 1 + segment.length(),
  )
  let data_len = coe_header_len + 1 + segment.length()
  let mbh : MailboxHeader = {
    length: data_len.reinterpret_as_uint(),
    address: mailbox_request_address(station),
    channel: b'\x00',
    priority: b'\x00',
    mb_type: CoE,
    counter,
  }
  let hdr_bytes = mbh.to_bytes()
  for i in 0..> 8) & 0xFFU).to_byte())
  let n_unused = 7 - segment.length()
  let mut cmd = b'\x00'
  if toggle {
    cmd = cmd | b'\x10'
  }
  cmd = cmd | (n_unused.to_byte() << 1)
  if last_segment {
    cmd = cmd | b'\x01'
  }
  buf.write_byte(cmd)
  for i in 0.. Bytes {
  let payload_len = coe_header_len + sdo_info_header_len + 2
  let buf = Buffer(size_hint=mailbox_header_len + payload_len)
  let mbh : MailboxHeader = {
    length: payload_len.reinterpret_as_uint(),
    address: mailbox_request_address(station),
    channel: b'\x00',
    priority: b'\x00',
    mb_type: CoE,
    counter,
  }
  let hdr = mbh.to_bytes()
  for i in 0..> 8) & 0xFFU).to_byte())
  // SDO Info header: opcode=0x01 (Get OD List req), incomplete=0, reserved=0, fragments_left=0
  buf.write_byte(b'\x01')
  buf.write_byte(b'\x00')
  buf.write_byte(b'\x00')
  buf.write_byte(b'\x00')
  // List type (WORD, LE)
  buf.write_byte((list_type & 0xFFU).to_byte())
  buf.write_byte(((list_type >> 8) & 0xFFU).to_byte())
  buf.to_bytes()
}

///|
/// Build an SDO Info "Get OD" (object description) request frame.
pub fn build_sdo_info_get_od_request(
  counter : Byte,
  station : UInt,
  index : UInt,
) -> Bytes {
  let payload_len = coe_header_len + sdo_info_header_len + 2
  let buf = Buffer(size_hint=mailbox_header_len + payload_len)
  let mbh : MailboxHeader = {
    length: payload_len.reinterpret_as_uint(),
    address: mailbox_request_address(station),
    channel: b'\x00',
    priority: b'\x00',
    mb_type: CoE,
    counter,
  }
  let hdr = mbh.to_bytes()
  for i in 0..> 8) & 0xFFU).to_byte())
  buf.write_byte(b'\x03')
  buf.write_byte(b'\x00')
  buf.write_byte(b'\x00')
  buf.write_byte(b'\x00')
  buf.write_byte((index & 0xFFU).to_byte())
  buf.write_byte(((index >> 8) & 0xFFU).to_byte())
  buf.to_bytes()
}

///|
/// Build an SDO Info "Get OE" (entry description) request frame.
/// `value_info` is a bitmask selecting additional fields in the response.
pub fn build_sdo_info_get_oe_request(
  counter : Byte,
  station : UInt,
  index : UInt,
  subindex : Byte,
  value_info : Byte,
) -> Bytes {
  let payload_len = coe_header_len + sdo_info_header_len + 4
  let buf = Buffer(size_hint=mailbox_header_len + payload_len)
  let mbh : MailboxHeader = {
    length: payload_len.reinterpret_as_uint(),
    address: mailbox_request_address(station),
    channel: b'\x00',
    priority: b'\x00',
    mb_type: CoE,
    counter,
  }
  let hdr = mbh.to_bytes()
  for i in 0..> 8) & 0xFFU).to_byte())
  buf.write_byte(b'\x05')
  buf.write_byte(b'\x00')
  buf.write_byte(b'\x00')
  buf.write_byte(b'\x00')
  buf.write_byte((index & 0xFFU).to_byte())
  buf.write_byte(((index >> 8) & 0xFFU).to_byte())
  buf.write_byte(subindex)
  buf.write_byte(value_info)
  buf.to_bytes()
}

///|
/// Decoded SDO Info response header and payload body.
pub(all) struct SdoInfoResponse {
  opcode : Byte
  incomplete : Bool
  fragments_left : UInt
  payload : Bytes
} derive(Eq, Debug)

///|
/// SDO Info object description (Get OD response body).
pub(all) struct SdoInfoObjectDescription {
  index : UInt
  data_type : UInt
  object_code : Byte
  max_subindex : Byte
  name : String
} derive(Eq, Debug)

///|
/// SDO Info entry description (Get OE response body).
pub(all) struct SdoInfoEntryDescription {
  index : UInt
  subindex : Byte
  value_info : Byte
  data_type : UInt
  bit_length : UInt
  access : UInt
  name : String
} derive(Eq, Debug)

///|
/// Decode SDO Info response frame (service=0x08).
pub fn decode_sdo_info_response(
  data : Bytes,
) -> (MailboxHeader, SdoInfoResponse) raise @hal.EcError {
  if data.length() < mailbox_header_len + coe_header_len + sdo_info_header_len {
    raise @hal.EcError::MailboxProtocol("SDO Info response frame too short")
  }
  let mbh = MailboxHeader::from_bytes(data)
  let payload_len = mbh.length.reinterpret_as_int()
  if payload_len < coe_header_len + sdo_info_header_len {
    raise @hal.EcError::MailboxProtocol("SDO Info response payload too short")
  }
  let frame_len = mailbox_header_len + payload_len
  if data.length() < frame_len {
    raise @hal.EcError::MailboxProtocol("SDO Info response truncated")
  }
  if mbh.mb_type != CoE {
    raise @hal.EcError::MailboxProtocol(
      "expected CoE mailbox, got " + @debug.to_string(mbh.mb_type),
    )
  }
  let off = mailbox_header_len
  let coe_word = data[off].to_uint() | (data[off + 1].to_uint() << 8)
  let svc_field = (coe_word >> 12) & 0xFU
  if svc_field != 8U {
    raise @hal.EcError::MailboxProtocol("expected SDO Info service (0x08)")
  }
  let info_off = off + coe_header_len
  let hdr0 = data[info_off]
  let opcode = hdr0 & b'\x7F'
  let incomplete = (hdr0 & b'\x80') != b'\x00'
  let fragments_left = data[info_off + 2].to_uint() |
    (data[info_off + 3].to_uint() << 8)
  let body_len = payload_len - coe_header_len - sdo_info_header_len
  let payload = if body_len > 0 {
    Bytes::makei(body_len, fn(i) { data[info_off + sdo_info_header_len + i] })
  } else {
    Bytes::new(0)
  }
  (mbh, { opcode, incomplete, fragments_left, payload })
}

///|
/// Decode OD list payload (sequence of little-endian WORD indices).
pub fn decode_sdo_info_od_list_payload(payload : Bytes) -> Array[UInt] {
  let count = payload.length() / 2
  Array::makei(count, fn(i) {
    let off = i * 2
    payload[off].to_uint() | (payload[off + 1].to_uint() << 8)
  })
}

///|
/// Decode Get OD response payload.
pub fn decode_sdo_info_od_description_payload(
  payload : Bytes,
) -> SdoInfoObjectDescription raise @hal.EcError {
  if payload.length() < 6 {
    raise @hal.EcError::MailboxProtocol("SDO Info Get OD payload too short")
  }
  let index = payload[0].to_uint() | (payload[1].to_uint() << 8)
  let data_type = payload[2].to_uint() | (payload[3].to_uint() << 8)
  let max_subindex = payload[4]
  let object_code = payload[5]
  let name = if payload.length() > 6 {
    let name_bytes = Bytes::makei(payload.length() - 6, fn(i) { payload[6 + i] })
    @utf8.decode(name_bytes) catch {
      _ =>
        raise @hal.EcError::MailboxProtocol(
          "SDO Info Get OD name is not valid UTF-8",
        )
    }
  } else {
    ""
  }
  { index, data_type, object_code, max_subindex, name }
}

///|
/// Decode Get OE response payload.
pub fn decode_sdo_info_oe_description_payload(
  payload : Bytes,
) -> SdoInfoEntryDescription raise @hal.EcError {
  if payload.length() < 10 {
    raise @hal.EcError::MailboxProtocol("SDO Info Get OE payload too short")
  }
  let index = payload[0].to_uint() | (payload[1].to_uint() << 8)
  let subindex = payload[2]
  let value_info = payload[3]
  let data_type = payload[4].to_uint() | (payload[5].to_uint() << 8)
  let bit_length = payload[6].to_uint() | (payload[7].to_uint() << 8)
  let access = payload[8].to_uint() | (payload[9].to_uint() << 8)
  let name = if payload.length() > 10 {
    let name_bytes = Bytes::makei(payload.length() - 10, fn(i) {
      payload[10 + i]
    })
    @utf8.decode(name_bytes) catch {
      _ =>
        raise @hal.EcError::MailboxProtocol(
          "SDO Info Get OE name is not valid UTF-8",
        )
    }
  } else {
    ""
  }
  { index, subindex, value_info, data_type, bit_length, access, name }
}

///|
/// Human-readable description of an SDO Abort Code per ETG.1000.6 Table 40.
pub fn sdo_abort_code_description(code : UInt) -> String {
  match code {
    0x00000000U => "No error"
    0x05030000U => "Toggle bit not changed"
    0x05040000U => "SDO protocol timeout"
    0x05040001U => "Client/Server command specifier not valid or unknown"
    0x05040005U => "Out of memory"
    0x06010000U => "Unsupported access to an object"
    0x06010001U => "Attempt to read a write-only object"
    0x06010002U => "Attempt to write a read-only object"
    0x06010003U => "Subindex cannot be written, SI0 must be 0 for write access"
    0x06010004U =>
      "SDO Complete Access not supported for variable length objects"
    0x06010005U => "Object length exceeds mailbox size"
    0x06010006U => "Object mapped to RxPDO, SDO download blocked"
    0x06020000U => "The object does not exist in the object dictionary"
    0x06040041U => "The object cannot be mapped into the PDO"
    0x06040042U =>
      "Number and length of objects to be mapped would exceed PDO length"
    0x06040043U => "General parameter incompatibility reason"
    0x06040047U => "General internal incompatibility in the device"
    0x06060000U => "Access failed due to a hardware error"
    0x06070010U =>
      "Data type does not match, length of service parameter does not match"
    0x06070012U =>
      "Data type does not match, length of service parameter too high"
    0x06070013U =>
      "Data type does not match, length of service parameter too low"
    0x06090011U => "Subindex does not exist"
    0x06090030U => "Value range of parameter exceeded"
    0x06090031U => "Value of parameter written too high"
    0x06090032U => "Value of parameter written too low"
    0x06090036U => "Maximum value is less than minimum value"
    0x08000000U => "General error"
    0x08000020U => "Data cannot be transferred or stored to the application"
    0x08000021U =>
      "Data cannot be transferred or stored to the application because of local control"
    0x08000022U =>
      "Data cannot be transferred or stored to the application because of the present device state"
    0x08000023U =>
      "Object dictionary dynamic generation fails or no object dictionary is present"
    _ => "Unknown (" + code.to_string() + ")"
  }
}