|
| 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 |
0 commit comments