Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

FCI v10 and FCI v5 on the wire

The crate speaks two versions of the Franka Control Interface. This page is the byte-level comparison and the negotiation between them. What the difference means for a program on a Franka Emika Robot is on FER / Panda specifics; the rate-limit envelopes are on Rate limiting and filtering.

Supported robots

FCI versionlibfranka semanticsRobotSystemRobotStateRobotCommandModel source
v100.20 – 0.21 (ported from 0.21.2)Franka Research 35.9.0 or later (server version 10)1377 B371 BURDF from GetRobotModel, evaluated natively
v50.9.2Franka Emika Robot / Panda (FER)4.2.1 or later (server version 5)2373 B370 Bshipped identified parameters, evaluated natively

Both versions share the framing: a TCP command channel on port 1337 with a 12-byte CommandHeader (command, command id, total size) and u8 status bytes, and a UDP channel on which the robot sends one RobotState per millisecond and receives one RobotCommand per state while a motion runs. The Franka Hand has its own protocol on port 1338 (10-byte header, u16 statuses), version 3 and byte-identical on both robots.

Version negotiation

Robot::new connects announcing library version 10. An FR3 accepts. An FER answers kIncompatibleLibraryVersion and reports version 5; the client then closes both sockets, opens a fresh TCP+UDP session and connects again as version 5. robot.fci_version() says which one you got:

extern crate franka;
use franka::{FciVersion, RealtimeConfig, Robot};

fn main() -> franka::FrankaResult<()> {
let robot = Robot::new("172.16.0.2", RealtimeConfig::Enforce)?;   // FR3 or FER
match robot.fci_version() {
    FciVersion::V10 => println!("FR3"),
    FciVersion::V5 => println!("FER"),
}
Ok(()) }

Auto-detection costs an FER one full connect, a rejected handshake and a disconnect, i.e. two FCI client slots in sequence. A supervisor that reconnects in a loop, or any program that knows its arm, pins the version with VersionPolicy::Exact:

extern crate franka;
use franka::{FciVersion, RealtimeConfig, Robot, RobotOptions, VersionPolicy};

fn main() -> franka::FrankaResult<()> {
let robot = Robot::with_options(
    "172.16.0.2",
    RobotOptions::new(RealtimeConfig::Enforce).with_version(VersionPolicy::Exact(FciVersion::V5)),
)?;
Ok(()) }

VersionPolicy::Exact(FciVersion::V10) against an FER never retries and fails with FrankaError::IncompatibleVersion { server_version: 5, library_version: 10 }, which is what a v10 libfranka reports when pointed at an FER. Any server version other than 5 or 10 is returned as the same error.

One deliberate divergence from libfranka, which only inspects the handshake’s status byte: a Connect reply of kSuccess that reports a version other than the one the client announced is refused with IncompatibleVersion. Everything after the handshake is decoded against the announced version, so such a session would fail on the first state with Protocol("libfranka: incorrect object size") instead of naming the problem. franka-sim’s FER build answers kSuccess with version 5 whatever the client announced, and this rule is what lets auto-detection recognise it.

After the handshake, an FCI v10 session fetches the URDF (GetRobotModel) and derives the position-dependent joint velocity limits from it, as libfranka 0.21.2 does; a v5 session stops after the first state, as 0.9.2 does.

What differs on the wire

FCI v5 (FER)FCI v10 (FR3)
libfranka0.9.20.21.2
RobotState datagram2373 bytes, every field f64, no accelerometer arrays1377 bytes, mixed f64/f32
RobotCommand datagram370 bytes: MotionGeneratorCommand 306 + ControllerCommand 56 (tau_J_d alone)371 bytes: ControllerCommand 57, also carries torque_command_finished
tau_J_d in the statef64f32
command numberingConnect 0, Move 1, StopMove 2, GetCartesianLimit 3, SetCollisionBehavior 4, SetJointImpedance 5, SetCartesianImpedance 6, SetGuidingMode 7, SetEEToK 8, SetNEToEE 9, SetLoad 10, SetFilters 11, AutomaticErrorRecovery 12, LoadModelLibrary 13Connect 0, Move 1, StopMove 2, SetCollisionBehavior 3, SetJointImpedance 4, SetCartesianImpedance 5, SetGuidingMode 6, SetEEToK 7, SetNEToEE 8, SetLoad 9, AutomaticErrorRecovery 10, GetRobotModel 11
Move request56 bytes113 bytes: adds use_async_motion_generator and seven maximum_velocity values, always transmitted
Move generator modesfour, no Nonethe same four plus None (4)
Move::Status10 values12: PreemptedDueToActivatedSafetyFunctions (3) and CommandRejectedDueToActivatedSafetyFunctions (4) inserted after Preempted (2), so ReflexAborted is 6 on v5 and 8 on v10
torque-only controljoint-velocity generator commanding zerosMotionGeneratorMode::None
SetFiltersyes (Robot::set_filters)removed from the protocol
GetCartesianLimityes (Robot::virtual_wall)removed from the protocol
GetRobotModel (URDF)no such commandyes (Robot::robot_model)
LoadModelLibraryyes (Robot::load_model_from_robot)no such command
joint velocity limitsflat: 2.175 rad/s (J1–4), 2.610 rad/s (J5–7)position-dependent envelope from the URDF
kTolNumberPacketsLost3.00.0
joint position limits (JOINT_POSITION_LIMITS, from the robots’ URDFs; the FR3 row is the robot URDF’s hard limits, a few mrad wider than franka_description’s datasheet values)J1 ±2.8973, J2 ±1.7628, J3 ±2.8973, J4 [−3.0718, −0.0698], J5 ±2.8973, J6 [−0.0175, 3.7525], J7 ±2.8973J1 ±2.7501, J2 ±1.7918, J3 ±2.9065, J4 [−3.0481, −0.1458], J5 ±2.8101, J6 [0.5409, 4.5205], J7 ±3.0196
gripper protocolidentical (version 3, port 1338)identical

The status enums other than Move::Status are shorter on v5 in the same way (no safety-function values); the crate maps every v5 byte onto the v10 name set by name, and a byte that is not valid for the negotiated version is FrankaError::Protocol("libfranka: Unexpected response while handling <name> command!").

Commands a version does not have

Calling one fails client-side, before a byte is sent, with libfranka’s own text:

FrankaError::InvalidOperation("libfranka: Get Robot Model is not available on FCI version 5.")
FrankaError::InvalidOperation("libfranka: Set Filters is not available on FCI version 10.")

Two public items carry the version: CommandName::command(version) -> Option<u32> is the wire id of a command under a version (None where it does not exist), and the FER’s rate-limit constants live only at franka::rate_limiting::fer::*, with the FR3’s at the crate root.

Two IP fragments per state

The v5 state datagram, 2373 bytes, exceeds the 1500-byte Ethernet MTU, so every FER state arrives as exactly two IP fragments: 2000 packets per second for the host to service instead of 1000. The FER hardware campaign saw no reassembly failures or drops (ReasmFails, ReasmTimeout, Udp.InErrors and NIC drops all zero); the doubled packet rate is the most plausible reason the FER lost occasional cycles where the FR3 lost none, as a load effect. See Benchmarks.

How the layout is pinned

Every wire struct is #[repr(C, packed)], and its size and every field offset are asserted against the declarations in libfranka’s service_types.h, rbk_types.h and the gripper’s types.h: crates/franka-rs/tests/wire_sizes.rs for FCI v10 (libfranka 0.21.2, including the 23-byte gripper state) and tests/wire_sizes_v5.rs for FCI v5 (libfranka 0.9.2, including the command numbering and the status tables). Both run offline, in CI’s check job. The mock-FCI unit tests under robot/mock_tests additionally pin the v5 Move payload, the torque-only datagrams and the LoadModelLibrary request byte for byte.

Wire parsing, the state demultiplexer, the rate limiter, the trajectory generator and the control loops are safe Rust; the crate’s unsafe is confined to the libc scheduler-priority calls in realtime.rs, the libc socket calls in network/udp.rs and network/tcp.rs, and the opt-in dlopen model loader (model/model_library.rs, model/so_backend/).