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}