Skip to main content

franka/wire/
mod.rs

1//! Packed, little-endian wire structs of the Franka Control Interface.
2//!
3//! Every struct in this module is a byte-for-byte port of the corresponding C++ struct in
4//! libfranka 0.21.2 (`common/include/research_interface/**`), which are declared inside
5//! `#pragma pack(push, 1)`. Rust equivalents are therefore `#[repr(C, packed)]` and use the
6//! explicitly little-endian scalar types of [`zerocopy::little_endian`] so that the layout is
7//! identical on any host. C++ `bool` members become `u8` (`0` = false, non-zero = true).
8//!
9//! Robot messages are versioned; FCI v10 lives in [`robot::v10`] and is re-exported as
10//! [`robot`]. The gripper protocol (version 3) lives in [`gripper`].
11
12pub mod gripper;
13pub mod robot;
14
15use zerocopy::little_endian::{F32, F64, U32};
16use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout, Unaligned};
17
18/// libfranka's `ProtocolException("libfranka: incorrect object size")` (`src/network.h:140`),
19/// raised whenever a received message does not have the length its type requires.
20pub(crate) fn incorrect_object_size() -> crate::error::FrankaError {
21    crate::error::FrankaError::Protocol("libfranka: incorrect object size".to_string())
22}
23
24/// FCI library version implemented by this crate (`research_interface::robot::kVersion`).
25pub const ROBOT_VERSION: u16 = 10;
26
27/// Robot command TCP port (`research_interface::robot::kCommandPort`).
28pub const ROBOT_COMMAND_PORT: u16 = 1337;
29
30/// Which of the two protocol header layouts a TCP session frames with.
31///
32/// libfranka has one `CommandHeader` per protocol: the robot's has a `uint32_t` command
33/// (`research_interface/robot/service_types.h`), the gripper's a `uint16_t` command
34/// (`research_interface/gripper/types.h`). The framing code is otherwise identical, so the
35/// Rust [`crate::network::TcpSession`] is generic over this enum instead of over a type.
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub enum HeaderLayout {
38    /// 12-byte header: `u32 command, u32 command_id, u32 size`.
39    Robot,
40    /// 10-byte header: `u16 command, u32 command_id, u32 size`.
41    Gripper,
42}
43
44impl HeaderLayout {
45    /// Size of the header in bytes (12 for the robot, 10 for the gripper).
46    pub const fn header_len(self) -> usize {
47        match self {
48            HeaderLayout::Robot => ROBOT_HEADER_LEN,
49            HeaderLayout::Gripper => GRIPPER_HEADER_LEN,
50        }
51    }
52
53    /// Width of the leading `command` field in bytes (4 for the robot, 2 for the gripper).
54    pub const fn command_width(self) -> usize {
55        match self {
56            HeaderLayout::Robot => 4,
57            HeaderLayout::Gripper => 2,
58        }
59    }
60
61    /// Writes a header for `command` into a fresh byte vector, reserving room for `payload_len`.
62    ///
63    /// `size` counts the header itself, exactly like libfranka's `CommandHeader::size`.
64    pub fn encode_header(self, command: u32, command_id: u32, payload_len: usize) -> Vec<u8> {
65        let size = (self.header_len() + payload_len) as u32;
66        let mut out = Vec::with_capacity(self.header_len() + payload_len);
67        match self {
68            HeaderLayout::Robot => out.extend_from_slice(&command.to_le_bytes()),
69            HeaderLayout::Gripper => out.extend_from_slice(&(command as u16).to_le_bytes()),
70        }
71        out.extend_from_slice(&command_id.to_le_bytes());
72        out.extend_from_slice(&size.to_le_bytes());
73        out
74    }
75
76    /// Decodes `command`, `command_id` and `size` out of a header-sized prefix of `buf`.
77    ///
78    /// Returns `None` when `buf` is shorter than [`HeaderLayout::header_len`].
79    pub fn decode_header(self, buf: &[u8]) -> Option<(u32, u32, u32)> {
80        if buf.len() < self.header_len() {
81            return None;
82        }
83        let (command, rest) = match self {
84            HeaderLayout::Robot => (
85                u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]),
86                &buf[4..],
87            ),
88            HeaderLayout::Gripper => (u16::from_le_bytes([buf[0], buf[1]]) as u32, &buf[2..]),
89        };
90        let command_id = u32::from_le_bytes([rest[0], rest[1], rest[2], rest[3]]);
91        let size = u32::from_le_bytes([rest[4], rest[5], rest[6], rest[7]]);
92        Some((command, command_id, size))
93    }
94}
95
96/// Size of the robot `CommandHeader` in bytes.
97pub const ROBOT_HEADER_LEN: usize = 12;
98
99/// Size of the gripper `CommandHeader` in bytes.
100pub const GRIPPER_HEADER_LEN: usize = 10;
101
102/// Robot command header (`research_interface::robot::CommandHeader`).
103///
104/// `size` is the total message length **including** this header.
105#[derive(Debug, Clone, Copy, FromBytes, IntoBytes, Immutable, KnownLayout, Unaligned)]
106#[repr(C, packed)]
107pub struct CommandHeader {
108    /// `research_interface::robot::Command` as a `u32`.
109    pub command: U32,
110    /// Identifier echoed by the server; used to demultiplex responses.
111    pub command_id: U32,
112    /// Total message size in bytes, header included.
113    pub size: U32,
114}
115
116impl CommandHeader {
117    /// Builds a header. `size` counts the header.
118    pub fn new(command: u32, command_id: u32, size: u32) -> Self {
119        CommandHeader {
120            command: U32::new(command),
121            command_id: U32::new(command_id),
122            size: U32::new(size),
123        }
124    }
125}
126
127/// Converts a wire array of little-endian `f32` to `[f64; N]`.
128///
129/// This is the Rust equivalent of libfranka's `RobotState::floatarray<N>::operator
130/// std::array<double, N>()` (`rbk_types.h`): the robot streams `float`, the public API is
131/// `double`.
132#[inline]
133pub fn f32s_to_f64<const N: usize>(src: &[F32; N]) -> [f64; N] {
134    let mut out = [0.0f64; N];
135    for (o, s) in out.iter_mut().zip(src.iter()) {
136        *o = s.get() as f64;
137    }
138    out
139}
140
141/// Converts a wire array of little-endian `f64` to `[f64; N]`.
142#[inline]
143pub fn f64s_to_f64<const N: usize>(src: &[F64; N]) -> [f64; N] {
144    let mut out = [0.0f64; N];
145    for (o, s) in out.iter_mut().zip(src.iter()) {
146        *o = s.get();
147    }
148    out
149}
150
151/// Converts `[f64; N]` to the little-endian wire representation.
152#[inline]
153pub fn f64s_to_wire<const N: usize>(src: &[f64; N]) -> [F64; N] {
154    let mut out = [F64::new(0.0); N];
155    for (o, &s) in out.iter_mut().zip(src.iter()) {
156        *o = F64::new(s);
157    }
158    out
159}
160
161/// Converts a wire `[[f32; 3]; 6]` accelerometer block to `[[f64; 3]; 6]`.
162#[inline]
163pub fn accel_to_f64(src: &[[F32; 3]; 6]) -> [[f64; 3]; 6] {
164    let mut out = [[0.0f64; 3]; 6];
165    for (o, s) in out.iter_mut().zip(src.iter()) {
166        *o = f32s_to_f64(s);
167    }
168    out
169}
170
171/// Returns the payload of a complete TCP message (everything after the header).
172///
173/// `message` must be a whole message as stored by [`crate::network::TcpSession`], i.e. the
174/// header followed by exactly `header.size - header_len` payload bytes.
175pub fn message_payload(layout: HeaderLayout, message: &[u8]) -> &[u8] {
176    &message[layout.header_len().min(message.len())..]
177}
178
179/// Parses a response payload out of a complete TCP message.
180///
181/// Mirrors libfranka's `Network::tcpBlockingReceiveResponse`, which only rejects a message when
182/// `header.size < sizeof(message)` — a longer message is accepted and the surplus is either
183/// handed out as variable-length data (`GetRobotModel`) or ignored. franka-sim pads its
184/// one-byte status responses to four bytes, so accepting trailing bytes is required in
185/// practice, not just for `GetRobotModel`.
186pub fn parse_response<T>(layout: HeaderLayout, message: &[u8]) -> crate::error::FrankaResult<T>
187where
188    T: FromBytes + KnownLayout + Immutable + Unaligned,
189{
190    let payload = message_payload(layout, message);
191    match T::read_from_prefix(payload) {
192        Ok((value, _rest)) => Ok(value),
193        Err(_) => Err(crate::error::FrankaError::Protocol(
194            "libfranka: Incorrect TCP message size.".to_string(),
195        )),
196    }
197}
198
199#[cfg(test)]
200mod tests {
201    use super::*;
202
203    #[test]
204    fn header_roundtrip() {
205        for layout in [HeaderLayout::Robot, HeaderLayout::Gripper] {
206            let bytes = layout.encode_header(3, 7, 5);
207            assert_eq!(bytes.len(), layout.header_len());
208            assert_eq!(
209                layout.decode_header(&bytes),
210                Some((3, 7, (layout.header_len() + 5) as u32))
211            );
212            assert_eq!(
213                layout.decode_header(&bytes[..layout.header_len() - 1]),
214                None
215            );
216        }
217    }
218
219    #[test]
220    fn robot_header_struct_matches_encode_header() {
221        let header = CommandHeader::new(1, 2, 12);
222        assert_eq!(
223            header.as_bytes(),
224            &HeaderLayout::Robot.encode_header(1, 2, 0)[..]
225        );
226    }
227}