1use std::mem::size_of;
13
14use zerocopy::{FromBytes, IntoBytes};
15
16use crate::duration::Duration;
17use crate::error::{FrankaError, FrankaResult};
18use crate::network::{connect_gripper, Network};
19use crate::wire::gripper::{
20 GraspRequest, GripperCommand, GripperCommandResponse, GripperMoveRequest,
21 GripperState as WireGripperState, GripperStatus,
22};
23use crate::wire::{parse_response, HeaderLayout};
24
25#[derive(Debug, Clone, Copy, PartialEq)]
27pub struct GripperState {
28 pub width: f64,
30 pub max_width: f64,
33 pub is_grasped: bool,
35 pub temperature: u16,
37 pub time: Duration,
39}
40
41impl From<&WireGripperState> for GripperState {
42 fn from(wire: &WireGripperState) -> Self {
44 GripperState {
45 width: wire.width.get(),
46 max_width: wire.max_width.get(),
47 is_grasped: wire.is_grasped != 0,
48 temperature: wire.temperature.get(),
49 time: Duration::from_millis(u64::from(wire.message_id.get())),
50 }
51 }
52}
53
54#[derive(Debug)]
63pub struct Gripper {
64 network: Network,
65 server_version: u16,
66}
67
68impl Gripper {
69 pub fn new(franka_address: &str) -> FrankaResult<Gripper> {
80 let (network, server_version) = connect_gripper(franka_address)?;
81 Ok(Gripper {
82 network,
83 server_version,
84 })
85 }
86
87 pub fn server_version(&self) -> u16 {
89 self.server_version
90 }
91
92 pub fn homing(&self) -> FrankaResult<bool> {
103 self.execute_command(GripperCommand::Homing, &[])
104 }
105
106 pub fn grasp(
118 &self,
119 width: f64,
120 speed: f64,
121 force: f64,
122 epsilon_inner: f64,
123 epsilon_outer: f64,
124 ) -> FrankaResult<bool> {
125 let request = GraspRequest::new(width, epsilon_inner, epsilon_outer, speed, force);
126 self.execute_command(GripperCommand::Grasp, request.as_bytes())
127 }
128
129 pub fn move_gripper(&self, width: f64, speed: f64) -> FrankaResult<bool> {
135 let request = GripperMoveRequest::new(width, speed);
136 self.execute_command(GripperCommand::Move, request.as_bytes())
137 }
138
139 pub fn stop(&self) -> FrankaResult<bool> {
145 self.execute_command(GripperCommand::Stop, &[])
146 }
147
148 pub fn read_once(&self) -> FrankaResult<GripperState> {
156 let mut buf = [0u8; size_of::<WireGripperState>()];
157 while self.network.udp.try_receive(&mut buf)?.is_some() {}
159
160 let n = self.network.udp.blocking_receive(&mut buf)?;
161 if n != buf.len() {
162 return Err(FrankaError::Protocol(
163 "libfranka: incorrect object size".to_string(),
164 ));
165 }
166 let wire_state = WireGripperState::read_from_bytes(&buf)
167 .map_err(|_| FrankaError::Protocol("libfranka: incorrect object size".to_string()))?;
168 Ok(GripperState::from(&wire_state))
169 }
170
171 fn execute_command(&self, command: GripperCommand, payload: &[u8]) -> FrankaResult<bool> {
174 let command_id = self
175 .network
176 .tcp
177 .send_request(command.to_u16() as u32, payload)?;
178 let message = self.network.tcp.blocking_receive_response(command_id)?;
179 let response: GripperCommandResponse = parse_response(HeaderLayout::Gripper, &message)?;
180
181 match GripperStatus::from_u16(response.status.get()) {
182 Some(GripperStatus::Success) => Ok(true),
183 Some(GripperStatus::Fail) => Err(FrankaError::Command(
184 "libfranka gripper: Command failed!".to_string(),
185 )),
186 Some(GripperStatus::Unsuccessful) => Ok(false),
187 Some(GripperStatus::Aborted) => Err(FrankaError::Command(
188 "libfranka gripper: Command aborted!".to_string(),
189 )),
190 None => Err(FrankaError::Protocol(
191 "libfranka gripper: Unexpected response while handling command!".to_string(),
192 )),
193 }
194 }
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200 use std::io::{Read, Write};
201 use std::net::{Ipv4Addr, SocketAddr, TcpListener, TcpStream, UdpSocket};
202 use std::time::Instant;
203
204 use zerocopy::little_endian::U16;
205
206 use crate::wire::gripper::{GripperConnectResponse, GripperConnectStatus, GRIPPER_VERSION};
207 use crate::wire::GRIPPER_HEADER_LEN;
208
209 fn accept_and_serve<F, T>(serve: F) -> (SocketAddr, std::thread::JoinHandle<T>)
212 where
213 F: FnOnce(TcpStream) -> T + Send + 'static,
214 T: Send + 'static,
215 {
216 let listener = TcpListener::bind("127.0.0.1:0").unwrap();
217 let addr = listener.local_addr().unwrap();
218 let handle = std::thread::spawn(move || {
219 let (stream, _) = listener.accept().unwrap();
220 serve(stream)
221 });
222 (addr, handle)
223 }
224
225 fn read_message(stream: &mut TcpStream) -> (u16, u32, Vec<u8>) {
227 let mut header = [0u8; GRIPPER_HEADER_LEN];
228 stream.read_exact(&mut header).unwrap();
229 let (command, command_id, size) = HeaderLayout::Gripper.decode_header(&header).unwrap();
230 let mut payload = vec![0u8; size as usize - GRIPPER_HEADER_LEN];
231 stream.read_exact(&mut payload).unwrap();
232 (command as u16, command_id, payload)
233 }
234
235 fn write_message(stream: &mut TcpStream, command: u16, command_id: u32, payload: &[u8]) {
237 let mut message =
238 HeaderLayout::Gripper.encode_header(command as u32, command_id, payload.len());
239 message.extend_from_slice(payload);
240 stream.write_all(&message).unwrap();
241 stream.flush().unwrap();
242 }
243
244 fn serve_connect<F, T>(
247 mut stream: TcpStream,
248 status: GripperConnectStatus,
249 version: u16,
250 then: F,
251 ) -> T
252 where
253 F: FnOnce(TcpStream) -> T,
254 {
255 let (command, command_id, payload) = read_message(&mut stream);
256 assert_eq!(command, GripperCommand::Connect.to_u16());
257 assert_eq!(payload.len(), 4, "Connect::Request is 4 bytes");
258 let response = GripperConnectResponse {
259 status: U16::new(status.to_u16()),
260 version: U16::new(version),
261 };
262 write_message(&mut stream, command, command_id, response.as_bytes());
263 then(stream)
264 }
265
266 #[test]
267 fn wire_gripper_state_is_23_bytes() {
268 assert_eq!(size_of::<WireGripperState>(), 23);
269 }
270
271 #[test]
272 fn connect_reports_server_version() {
273 let (addr, server) = accept_and_serve(|stream| {
274 serve_connect(
275 stream,
276 GripperConnectStatus::Success,
277 GRIPPER_VERSION,
278 |stream| {
279 std::thread::sleep(std::time::Duration::from_millis(50));
280 drop(stream);
281 },
282 );
283 });
284
285 let gripper = Gripper::new(&addr.to_string()).expect("handshake failed");
286 assert_eq!(gripper.server_version(), GRIPPER_VERSION);
287
288 drop(gripper);
289 server.join().unwrap();
290 }
291
292 #[test]
294 fn connect_reports_incompatible_version_like_libfranka() {
295 let (addr, server) = accept_and_serve(|stream| {
296 serve_connect(
297 stream,
298 GripperConnectStatus::IncompatibleLibraryVersion,
299 7,
300 |stream| {
301 std::thread::sleep(std::time::Duration::from_millis(50));
302 drop(stream);
303 },
304 );
305 });
306
307 let error = Gripper::new(&addr.to_string()).expect_err("expected a handshake failure");
308 match error {
309 FrankaError::IncompatibleVersion {
310 server_version,
311 library_version,
312 } => {
313 assert_eq!(server_version, 7);
314 assert_eq!(library_version, GRIPPER_VERSION);
315 }
316 other => panic!("expected IncompatibleVersion, got {other:?}"),
317 }
318 assert_eq!(
319 error.to_string(),
320 format!(
321 "libfranka: Incompatible library version (server version: 7, library version: {GRIPPER_VERSION})."
322 )
323 );
324
325 server.join().unwrap();
326 }
327
328 fn mock_command_server(raw_status: u16) -> (Gripper, std::thread::JoinHandle<u16>) {
332 let (addr, server) = accept_and_serve(move |stream| {
333 serve_connect(
334 stream,
335 GripperConnectStatus::Success,
336 GRIPPER_VERSION,
337 move |mut stream| {
338 let (command, command_id, _payload) = read_message(&mut stream);
339 let response = GripperCommandResponse {
340 status: U16::new(raw_status),
341 };
342 write_message(&mut stream, command, command_id, response.as_bytes());
343 std::thread::sleep(std::time::Duration::from_millis(50));
344 command
345 },
346 )
347 });
348 let gripper = Gripper::new(&addr.to_string()).expect("handshake failed");
349 (gripper, server)
350 }
351
352 enum Expect {
354 Ok(bool),
355 Command(&'static str),
356 Protocol,
357 }
358
359 #[test]
364 fn command_status_mapping() {
365 type Run = fn(&Gripper) -> FrankaResult<bool>;
366 let commands: [(&str, u16, Run); 4] = [
367 ("homing", GripperCommand::Homing.to_u16(), |g| g.homing()),
368 ("move", GripperCommand::Move.to_u16(), |g| {
369 g.move_gripper(0.05, 0.1)
370 }),
371 ("grasp", GripperCommand::Grasp.to_u16(), |g| {
372 g.grasp(0.05, 0.1, 400.0, 0.004, 0.005)
373 }),
374 ("stop", GripperCommand::Stop.to_u16(), |g| g.stop()),
375 ];
376
377 let cases: [(u16, Expect); 5] = [
378 (GripperStatus::Success.to_u16(), Expect::Ok(true)),
379 (
380 GripperStatus::Fail.to_u16(),
381 Expect::Command("libfranka gripper: Command failed!"),
382 ),
383 (GripperStatus::Unsuccessful.to_u16(), Expect::Ok(false)),
384 (
385 GripperStatus::Aborted.to_u16(),
386 Expect::Command("libfranka gripper: Command aborted!"),
387 ),
388 (99, Expect::Protocol),
389 ];
390
391 for (name, expected_command, run) in commands {
392 for (raw_status, expect) in &cases {
393 let (gripper, server) = mock_command_server(*raw_status);
394 let result = run(&gripper);
395 let received_command = server.join().unwrap();
396 assert_eq!(
397 received_command, expected_command,
398 "{name}: wrong command byte on the wire"
399 );
400
401 match expect {
402 Expect::Ok(expected) => {
403 assert_eq!(result.unwrap(), *expected, "{name}, status {raw_status}")
404 }
405 Expect::Command(message) => {
406 let error = result.unwrap_err();
407 assert_eq!(&error.to_string(), message, "{name}, status {raw_status}");
408 assert!(matches!(error, FrankaError::Command(_)));
409 }
410 Expect::Protocol => {
411 let error = result.unwrap_err();
412 assert_eq!(
413 error.to_string(),
414 "libfranka gripper: Unexpected response while handling command!",
415 "{name}, status {raw_status}"
416 );
417 assert!(matches!(error, FrankaError::Protocol(_)));
418 }
419 }
420
421 drop(gripper);
422 }
423 }
424 }
425
426 fn wire_state(message_id: u32, width: f64) -> WireGripperState {
428 WireGripperState {
429 message_id: zerocopy::little_endian::U32::new(message_id),
430 width: zerocopy::little_endian::F64::new(width),
431 max_width: zerocopy::little_endian::F64::new(0.08),
432 is_grasped: 0,
433 temperature: U16::new(25),
434 }
435 }
436
437 #[test]
438 fn read_once_drains_queued_datagrams_then_blocks_for_a_fresh_one() {
439 let (addr, server) = accept_and_serve(|mut stream| {
442 let (command, command_id, payload) = read_message(&mut stream);
443 assert_eq!(command, GripperCommand::Connect.to_u16());
444 assert_eq!(payload.len(), 4, "Connect::Request is 4 bytes");
445 let udp_port = u16::from_le_bytes([payload[2], payload[3]]);
446
447 let response = GripperConnectResponse {
448 status: U16::new(GripperConnectStatus::Success.to_u16()),
449 version: U16::new(GRIPPER_VERSION),
450 };
451 write_message(&mut stream, command, command_id, response.as_bytes());
452 std::thread::sleep(std::time::Duration::from_millis(50));
453 drop(stream);
454 udp_port
455 });
456
457 let gripper = Gripper::new(&addr.to_string()).expect("handshake failed");
458 let udp_port = server.join().unwrap();
459
460 let udp_server = UdpSocket::bind((Ipv4Addr::LOCALHOST, 0)).unwrap();
461 let peer: SocketAddr = (Ipv4Addr::LOCALHOST, udp_port).into();
462
463 for id in 1..=3u32 {
465 udp_server
466 .send_to(wire_state(id, 0.01).as_bytes(), peer)
467 .unwrap();
468 }
469 std::thread::sleep(std::time::Duration::from_millis(50));
470
471 let sender = std::thread::spawn(move || {
474 std::thread::sleep(std::time::Duration::from_millis(150));
475 udp_server
476 .send_to(wire_state(4, 0.02).as_bytes(), peer)
477 .unwrap();
478 });
479
480 let started = Instant::now();
481 let state = gripper.read_once().expect("read_once failed");
482 let elapsed = started.elapsed();
483
484 assert!((state.width - 0.02).abs() < 1e-9, "got {}", state.width);
485 assert_eq!(state.time.as_millis(), 4);
486 assert!(
487 elapsed >= std::time::Duration::from_millis(100),
488 "read_once returned after {elapsed:?}; it should have blocked for the fresh datagram \
489 instead of returning stale data"
490 );
491
492 sender.join().unwrap();
493 drop(gripper);
494 }
495}