Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
Releases before 1.0.0 are documented in the
[GitHub releases](https://github.com/JeanElsner/panda-py/releases).

## [Unreleased]

### Fixed

- `stop_controller()` could deadlock. It waited for the control thread while
holding the GIL, and the control thread takes the GIL to log, for instance
when its loop ends with an error such as a reflex during the stop, so neither
could proceed. It now releases the GIL while waiting, as does the `Panda`
destructor, which takes the same path.
- `get_log()` stalled the 1 kHz control loop for as long as it took to copy the
log, because both used the same lock; with a 60 s log that was long enough
for the robot to abort the motion. The log now has its own lock, which the
control loop only tries to take, so a sample arriving during a read is not
logged instead of the loop waiting. Reading 17,500 samples during control
now leaves a 3.6 ms gap between commands, against 23.8 ms before.

## [1.1.0] - 2026-09-30

Fixes the segfault in every `move_to_start`, `move_to_joint_position` and
Expand Down
3 changes: 3 additions & 0 deletions include/panda.h
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,9 @@ class Panda {
std::shared_ptr<franka::Model> model_;
franka::RobotState state_;
std::mutex mux_;
// Guards the log alone. The control thread only ever try-locks it, so
// reading a long log cannot stall the 1 kHz loop; it skips logging instead.
std::mutex log_mux_;
std::mutex error_mux_;
std::mutex read_mux_;
std::shared_ptr<TorqueController> current_controller_;
Expand Down
27 changes: 20 additions & 7 deletions src/panda.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -176,14 +176,14 @@ franka::RobotState Panda::getState() {
}

void Panda::enableLogging(size_t buffer_size) {
std::lock_guard<std::mutex> lock(mux_);
std::lock_guard<std::mutex> lock(log_mux_);
log_enabled_ = true;
log_size_ = buffer_size;
log_.clear();
}

void Panda::disableLogging() {
std::lock_guard<std::mutex> lock(mux_);
std::lock_guard<std::mutex> lock(log_mux_);
log_enabled_ = false;
}

Expand All @@ -193,7 +193,7 @@ std::map<std::string, std::list<Eigen::VectorXd>> Panda::getLog() {
std::map<std::string, std::list<Eigen::VectorXd>> log;
std::list<Eigen::VectorXd> O_T_EE, elbow, tau_J, control_command_success_rate,
O_F_ext_hat_K, K_F_ext_hat_K, q, dq, tau_ext_hat_filtered, time;
std::lock_guard<std::mutex> lock(mux_);
std::lock_guard<std::mutex> lock(log_mux_);
for (auto l : log_) {
O_T_EE.push_back(Eigen::Map<Eigen::VectorXd>(l.O_T_EE.data(), 16, 1));
elbow.push_back(Eigen::Map<Eigen::VectorXd>(l.elbow.data(), 2, 1));
Expand Down Expand Up @@ -298,9 +298,14 @@ Eigen::Matrix4d Panda::getPose() {
}

void Panda::_setState(const franka::RobotState& state) {
std::lock_guard<std::mutex> lock(mux_);
state_ = state;
if (log_enabled_) {
{
std::lock_guard<std::mutex> lock(mux_);
state_ = state;
}
// Called from the 1 kHz control loop, which must never wait for get_log()
// copying a long log. While a read holds the log, this sample is not logged.
std::unique_lock<std::mutex> log_lock(log_mux_, std::try_to_lock);
if (log_lock.owns_lock() && log_enabled_) {
log_.push_back(state);
if (log_.size() > log_size_) {
log_.pop_front();
Expand Down Expand Up @@ -357,7 +362,15 @@ void Panda::stopController() {
current_controller_->stop(getState(), model_);
}
if (current_thread_.joinable()) {
current_thread_.join();
// The control thread takes the GIL to log, for instance when its loop ends
// with an error, so it cannot finish while this thread holds the GIL. This
// is reached with the GIL held from Python and from the destructor.
if (PyGILState_Check()) {
py::gil_scoped_release release;
current_thread_.join();
} else {
current_thread_.join();
}
}
}

Expand Down
Loading