Skip to content

Commit dc678f4

Browse files
committed
Pull request #282: wip: changes for the mobile robot urdf getter
Merge in MOCTRL/libfranka from PRCUN-5824-mobile-model-urdf to main * commit '858daa4d486250ba33dc2a9c2549a97762acce66': remove the example update changelog Adds URDF robot type support Update test utilities and fix existing tests Add mobile robot example and test data Implement MobileModel class for mobile robot kinematics Update Model class to reject mobile robot URDFs Add mobile robot detection to Robot class Add URDF robot type detection utility
2 parents 3cc708c + 858daa4 commit dc678f4

22 files changed

+1021
-27
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22

33
All notable changes to libfranka in this file.
44

5+
## Unreleased
6+
7+
- Add `franka::MobileModel` class for mobile robot forward kinematics (swerve drive module poses via Pinocchio).
8+
- `franka::Model` now rejects mobile robot URDFs with a `ModelException`; use `franka::MobileModel` instead.
9+
- `Robot::loadModel()` throws `InvalidOperationException` for mobile robots; use `Robot::getRobotModel()` with `MobileModel` directly.
10+
511
## [0.20.5]
612
### libfranka - C++
713
- fix: Sometimes the PTP motion could get stuck in 'idle' after reaching the goal while doing async PTP motions

CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,8 @@ add_library(franka SHARED
109109
src/vacuum_gripper.cpp
110110
src/vacuum_gripper_state.cpp
111111
src/robot_model.cpp
112+
src/urdf_robot_type.cpp
113+
src/mobile_model.cpp
112114

113115
src/async_control/async_position_control_handler.cpp
114116

include/franka/mobile_model.h

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
// Copyright (c) 2026 Franka Robotics GmbH
2+
// Use of this source code is governed by the Apache-2.0 license, see LICENSE
3+
#pragma once
4+
5+
#include <array>
6+
#include <cstdint>
7+
#include <memory>
8+
#include <string>
9+
10+
/**
11+
* @file mobile_model.h
12+
* Contains the franka::MobileModel type for mobile robot kinematics.
13+
*/
14+
15+
namespace franka {
16+
17+
/**
18+
* Enumerates the frames of a mobile robot's drive modules.
19+
*/
20+
enum class MobileFrame : std::uint8_t {
21+
kFrontDriveModule, ///< Front swerve drive module frame
22+
kRearDriveModule ///< Rear swerve drive module frame
23+
};
24+
25+
/// Number of swerve drive modules.
26+
inline constexpr size_t kNumDriveModules = 2;
27+
28+
/// Number of joints per drive module (steering + drive).
29+
inline constexpr size_t kJointsPerDriveModule = 2;
30+
31+
/**
32+
* Joint positions for all controllable subsystems of a mobile robot.
33+
*
34+
* All arrays are default-initialized to zero (neutral configuration).
35+
*/
36+
struct MobileJointPositions {
37+
/// Drive module joint positions: {front_steering, front_drive, rear_steering, rear_drive}.
38+
std::array<double, kNumDriveModules * kJointsPerDriveModule> drive_modules{};
39+
};
40+
41+
/**
42+
* Calculates poses for mobile robot frames using Pinocchio.
43+
*
44+
* This class provides forward kinematics for mobile robots. It is constructed from a mobile robot
45+
* URDF and will reject arm robot URDFs.
46+
*
47+
* @note The pose() method is RT-safe (zero heap allocation). Passive joints (casters, rocker arm)
48+
* are held at their neutral configuration internally.
49+
*/
50+
class MobileModel final {
51+
public:
52+
/// Number of swerve drive modules.
53+
static constexpr size_t kNumModules = kNumDriveModules;
54+
55+
/// Number of joints per drive module (steering + drive).
56+
static constexpr size_t kJointsPerModule = kJointsPerDriveModule;
57+
58+
/**
59+
* Constructs a MobileModel from a URDF string.
60+
*
61+
* @param[in] urdf_model The URDF model string describing a mobile robot.
62+
*
63+
* @throw ModelException if the URDF describes an arm robot rather than a mobile robot.
64+
*/
65+
explicit MobileModel(const std::string& urdf_model);
66+
67+
/**
68+
* Move-constructs a new MobileModel instance.
69+
*
70+
* @param[in] other Other MobileModel instance.
71+
*/
72+
MobileModel(MobileModel&& other) noexcept;
73+
74+
/**
75+
* Move-assigns this MobileModel from another MobileModel instance.
76+
*
77+
* @param[in] other Other MobileModel instance.
78+
*
79+
* @return MobileModel instance.
80+
*/
81+
MobileModel& operator=(MobileModel&& other) noexcept;
82+
83+
/**
84+
* Destructor.
85+
*/
86+
~MobileModel() noexcept;
87+
88+
/// @cond DO_NOT_DOCUMENT
89+
MobileModel(const MobileModel&) = delete;
90+
MobileModel& operator=(const MobileModel&) = delete;
91+
/// @endcond
92+
93+
/**
94+
* Gets the 4x4 pose matrix for the given mobile frame relative to the robot's base frame
95+
* (URDF root link, body-fixed).
96+
*
97+
* The pose is represented as a 4x4 matrix in column-major format.
98+
* This method is RT-safe: it performs zero heap allocation.
99+
*
100+
* @param[in] frame The desired drive module frame (kFrontDriveModule or kRearDriveModule).
101+
* @param[in] joint_positions Joint positions for all controllable subsystems. Angles are in
102+
* radians; the conversion to the internal Pinocchio representation is handled automatically.
103+
*
104+
* @return Vectorized 4x4 pose matrix, column-major.
105+
*/
106+
std::array<double, 16> pose(MobileFrame frame, const MobileJointPositions& joint_positions) const;
107+
108+
private:
109+
class Data;
110+
std::unique_ptr<Data> data_;
111+
};
112+
113+
} // namespace franka

include/franka/robot.h

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -778,12 +778,23 @@ class Robot {
778778
* @}
779779
*/
780780

781+
/**
782+
* Returns whether the connected robot is a mobile robot (TMR).
783+
*
784+
* This is detected at connection time from the robot's URDF.
785+
*
786+
* @return true if the robot is a mobile robot, false if it is an arm robot.
787+
*/
788+
bool isMobileRobot() const noexcept;
789+
781790
/**
782791
* Loads the model library from the robot.
783792
*
784793
* @return Model instance.
785794
*
786795
* @throw ModelException if the model library cannot be loaded.
796+
* @throw InvalidOperationException if called on a mobile robot. Use
797+
* franka::MobileModel with the URDF from getRobotModel() instead.
787798
* @throw NetworkException if the connection is lost, e.g. after a timeout.
788799
*/
789800
Model loadModel();

src/mobile_model.cpp

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
// Copyright (c) 2026 Franka Robotics GmbH
2+
// Use of this source code is governed by the Apache-2.0 license, see LICENSE
3+
#include <franka/exception.h>
4+
#include <franka/mobile_model.h>
5+
6+
#include <algorithm>
7+
#include <cmath>
8+
#include <stdexcept>
9+
10+
// Ignore warnings from Pinocchio includes
11+
#if defined(__GNUC__)
12+
#pragma GCC diagnostic push
13+
#pragma GCC diagnostic ignored "-Wmaybe-uninitialized"
14+
#endif
15+
#include <pinocchio/algorithm/kinematics.hpp>
16+
#include <pinocchio/multibody/data.hpp>
17+
#include <pinocchio/multibody/model.hpp>
18+
#include <pinocchio/parsers/urdf.hpp>
19+
#if defined(__GNUC__)
20+
#pragma GCC diagnostic pop
21+
#endif
22+
23+
#include <Eigen/Core>
24+
25+
#include "urdf_robot_type.h"
26+
27+
namespace franka {
28+
29+
class MobileModel::Data {
30+
public:
31+
static constexpr size_t kNumDriveJoints = kNumModules * kJointsPerModule;
32+
33+
// Upper bound on Pinocchio configuration vector dimension (nq) for any supported mobile robot
34+
// URDF. Continuous joints use 2 config elements (cos, sin); revolute joints use 1. The TMR
35+
// URDF has ~15 joints, so nq is well below this limit. Using a fixed-max-size Eigen vector
36+
// keeps the storage inline and guarantees zero heap allocation in the RT hot path (pose()).
37+
static constexpr int kMaxConfigDim = 64;
38+
using ConfigVector = Eigen::Matrix<double, Eigen::Dynamic, 1, 0, kMaxConfigDim, 1>;
39+
40+
pinocchio::Model model;
41+
mutable pinocchio::Data data;
42+
43+
// Pinocchio joint indices for the drive module frames, looked up by name at construction.
44+
std::array<pinocchio::JointIndex, MobileModel::kNumModules> module_joint_indices{};
45+
46+
// Mapping from drive joint index to Pinocchio config index for the 4 drive module joints.
47+
struct JointMapping {
48+
int idx_q; // Index into the Pinocchio configuration vector
49+
bool is_continuous; // true if nq==2 (cos/sin pair), false if nq==1
50+
};
51+
std::array<JointMapping, kNumDriveJoints> joint_mappings{};
52+
53+
// Pre-allocated configuration buffer (neutral config for passive joints, updated for active).
54+
// Uses fixed-max-size Eigen vector: storage is inline, zero heap allocation at runtime.
55+
mutable ConfigVector q_buffer;
56+
57+
explicit Data(const std::string& urdf_model) {
58+
if (!isMobileRobotUrdf(urdf_model)) {
59+
throw ModelException(
60+
"libfranka: Cannot create MobileModel: the provided URDF does not describe "
61+
"a mobile robot (robot name must start with 'tmr').");
62+
}
63+
64+
pinocchio::urdf::buildModelFromXML(urdf_model, model);
65+
data = pinocchio::Data(model);
66+
67+
if (model.nq > kMaxConfigDim) {
68+
throw ModelException("libfranka: MobileModel: URDF configuration dimension (" +
69+
std::to_string(model.nq) + ") exceeds maximum (" +
70+
std::to_string(kMaxConfigDim) + ").");
71+
}
72+
73+
// Initialize q_buffer with neutral configuration.
74+
// Revolute joints: 0.0, Continuous joints: (cos(0)=1, sin(0)=0).
75+
q_buffer = ConfigVector::Zero(model.nq);
76+
for (int j = 1; j < model.njoints; ++j) {
77+
if (model.joints[j].nv() == 1 && model.joints[j].nq() == 2) {
78+
q_buffer[model.joints[j].idx_q()] = 1.0; // cos(0)
79+
q_buffer[model.joints[j].idx_q() + 1] = 0.0; // sin(0)
80+
}
81+
}
82+
83+
// Find the drive module frame joints by name suffix.
84+
// Convention: front drive module ends with "_joint_1", rear with "_joint_3".
85+
constexpr std::array<const char*, kNumModules> kModuleSuffixes = {"_joint_1", "_joint_3"};
86+
87+
for (size_t module_idx = 0; module_idx < kNumModules; ++module_idx) {
88+
const std::string suffix(kModuleSuffixes[module_idx]);
89+
bool found = false;
90+
for (pinocchio::JointIndex j = 1; j < static_cast<pinocchio::JointIndex>(model.njoints);
91+
++j) {
92+
const std::string& name = model.names[j];
93+
if (name.size() >= suffix.size() &&
94+
name.compare(name.size() - suffix.size(), suffix.size(), suffix) == 0) {
95+
module_joint_indices[module_idx] = j;
96+
found = true;
97+
break;
98+
}
99+
}
100+
if (!found) {
101+
throw ModelException(
102+
"libfranka: MobileModel: could not find drive module joint ending with '" + suffix +
103+
"' in the URDF.");
104+
}
105+
}
106+
107+
// Find and map the 4 drive module joints (steering + drive per module).
108+
// User array order: {front_steering, front_drive, rear_steering, rear_drive}
109+
// Convention: suffixes "_joint_0", "_joint_1", "_joint_2", "_joint_3".
110+
constexpr std::array<const char*, kNumDriveJoints> kJointSuffixes = {"_joint_0", "_joint_1",
111+
"_joint_2", "_joint_3"};
112+
113+
for (size_t i = 0; i < kNumDriveJoints; ++i) {
114+
const std::string suffix(kJointSuffixes[i]);
115+
bool found = false;
116+
for (pinocchio::JointIndex j = 1; j < static_cast<pinocchio::JointIndex>(model.njoints);
117+
++j) {
118+
const std::string& name = model.names[j];
119+
if (name.size() >= suffix.size() &&
120+
name.compare(name.size() - suffix.size(), suffix.size(), suffix) == 0) {
121+
joint_mappings[i].idx_q = model.joints[j].idx_q();
122+
joint_mappings[i].is_continuous =
123+
(model.joints[j].nq() == 2 && model.joints[j].nv() == 1);
124+
found = true;
125+
break;
126+
}
127+
}
128+
if (!found) {
129+
throw ModelException("libfranka: MobileModel: could not find joint ending with '" + suffix +
130+
"' in the URDF.");
131+
}
132+
}
133+
}
134+
};
135+
136+
MobileModel::MobileModel(const std::string& urdf_model)
137+
: data_(std::make_unique<Data>(urdf_model)) {}
138+
139+
MobileModel::MobileModel(MobileModel&&) noexcept = default;
140+
MobileModel& MobileModel::operator=(MobileModel&&) noexcept = default;
141+
MobileModel::~MobileModel() noexcept = default;
142+
143+
std::array<double, 16> MobileModel::pose(MobileFrame frame,
144+
const MobileJointPositions& joint_positions) const {
145+
// Update only the 4 drive module joints in the pre-allocated buffer. Zero heap allocation.
146+
for (size_t i = 0; i < kNumModules * kJointsPerModule; ++i) {
147+
const auto& mapping = data_->joint_mappings[i];
148+
if (mapping.is_continuous) {
149+
data_->q_buffer[mapping.idx_q] = std::cos(joint_positions.drive_modules[i]);
150+
data_->q_buffer[mapping.idx_q + 1] = std::sin(joint_positions.drive_modules[i]);
151+
} else {
152+
data_->q_buffer[mapping.idx_q] = joint_positions.drive_modules[i];
153+
}
154+
}
155+
156+
pinocchio::forwardKinematics(data_->model, data_->data, data_->q_buffer);
157+
158+
// Map MobileFrame to Pinocchio joint index via the pre-computed lookup table
159+
size_t module_index = 0;
160+
switch (frame) {
161+
case MobileFrame::kFrontDriveModule:
162+
module_index = 0;
163+
break;
164+
case MobileFrame::kRearDriveModule:
165+
module_index = 1;
166+
break;
167+
default:
168+
throw std::invalid_argument("libfranka: Invalid MobileFrame given.");
169+
}
170+
171+
pinocchio::JointIndex joint_index = data_->module_joint_indices[module_index];
172+
173+
std::array<double, 16> result;
174+
Eigen::Map<Eigen::Matrix4d>(result.data()) = data_->data.oMi[joint_index].toHomogeneousMatrix();
175+
return result;
176+
}
177+
178+
} // namespace franka

src/model.cpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Use of this source code is governed by the Apache-2.0 license, see LICENSE
33
#include "robot_model.h"
44

5+
#include <franka/exception.h>
56
#include <franka/model.h>
67

78
#include <iostream>
@@ -13,6 +14,8 @@
1314
#include <fstream>
1415
#include <sstream>
1516

17+
#include "urdf_robot_type.h"
18+
1619
using namespace std::string_literals; // NOLINT(google-build-using-namespace)
1720

1821
namespace franka {
@@ -24,6 +27,11 @@ Frame operator++(Frame& frame, int /* dummy */) noexcept {
2427
}
2528

2629
Model::Model(const std::string& urdf_model) {
30+
if (isMobileRobotUrdf(urdf_model)) {
31+
throw ModelException(
32+
"libfranka: Cannot create arm Model from a mobile robot URDF. "
33+
"Use franka::MobileModel instead.");
34+
}
2735
robot_model_ = std::make_unique<RobotModel>(urdf_model);
2836
}
2937

src/robot.cpp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,7 +338,16 @@ void Robot::stop() {
338338
impl_->executeCommand<research_interface::robot::StopMove>();
339339
}
340340

341+
bool Robot::isMobileRobot() const noexcept {
342+
return impl_->isMobileRobot();
343+
}
344+
341345
Model Robot::loadModel() {
346+
if (impl_->isMobileRobot()) {
347+
throw InvalidOperationException(
348+
"libfranka: loadModel() is not available for mobile robots. "
349+
"Use franka::MobileModel with the URDF from getRobotModel() instead.");
350+
}
342351
return impl_->loadModel(getRobotModel());
343352
}
344353

0 commit comments

Comments
 (0)