diff --git a/include/ur_client_library/ur/dashboard_client_implementation.h b/include/ur_client_library/ur/dashboard_client_implementation.h index 9979f529a..6fda79375 100644 --- a/include/ur_client_library/ur/dashboard_client_implementation.h +++ b/include/ur_client_library/ur/dashboard_client_implementation.h @@ -135,6 +135,27 @@ class DashboardClientImpl */ virtual void setReceiveTimeout([[maybe_unused]] const timeval& timeout) {}; + /*! + * \brief Gets the configured send timeout. If send timeout is unconfigured "normal" socket + * timeout of 1 second will be returned. + * + * \returns configured send timeout + */ + virtual timeval getConfiguredSendTimeout() const + { + timeval tv; + tv.tv_sec = 1; + tv.tv_usec = 0; + return tv; + } + + /*! + * \brief Sets the send timeout for the socket. + * + * \param timeout The timeout to be set + */ + virtual void setSendTimeout([[maybe_unused]] const timeval& timeout) {}; + /*! * \brief Sends command and verifies that a valid answer is received. * diff --git a/include/ur_client_library/ur/dashboard_client_implementation_x.h b/include/ur_client_library/ur/dashboard_client_implementation_x.h index d99ca7e3a..b257e1726 100644 --- a/include/ur_client_library/ur/dashboard_client_implementation_x.h +++ b/include/ur_client_library/ur/dashboard_client_implementation_x.h @@ -162,9 +162,10 @@ class DashboardClientImplX : public DashboardClientImpl DashboardResponse commandUpdateProgram(const std::string& file_path) override; DashboardResponse commandDownloadProgram(const std::string& program_name, const std::string& save_path) override; - void setReceiveTimeout([[maybe_unused]] const timeval& timeout) override - { - } + // Defined in the .cpp because httplib::Client is only forward-declared in this header. + void setReceiveTimeout(const timeval& timeout) override; + void setSendTimeout(const timeval& timeout) override; + timeval getConfiguredSendTimeout() const override; protected: DashboardResponse performProgramUpload( @@ -185,6 +186,8 @@ class DashboardClientImplX : public DashboardClientImpl std::unique_ptr cli_; VersionInformation robot_api_version_; + timeval recv_timeout_ = { 10, 0 }; + timeval send_timeout_ = { 10, 0 }; }; } // namespace urcl diff --git a/src/ur/dashboard_client_implementation_x.cpp b/src/ur/dashboard_client_implementation_x.cpp index 3826f2f33..d728d91d5 100644 --- a/src/ur/dashboard_client_implementation_x.cpp +++ b/src/ur/dashboard_client_implementation_x.cpp @@ -24,6 +24,7 @@ // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE // POSSIBILITY OF SUCH DAMAGE. +#include #include #include #include @@ -49,6 +50,38 @@ DashboardClientImplX::DashboardClientImplX(const std::string& host) : DashboardC // Some targets have changed between versions redirecting to the correct endpoint. For this to // work, we'll have to follow redirects, which is not the default for httplib. cli_->set_follow_location(true); + + // cpp-httplib's default read_timeout is 300 seconds. Applied unchanged, this makes any + // dashboard call hang for 5 minutes when the controller becomes unresponsive (e.g. network + // partition, paused container in tests). + // + // The 10 s default for read/write is chosen to cover blocking calls that legitimately take + // time on real hardware — brake_release, commandLoadProgram (read), commandUploadProgram + // (write), commandUpdateProgram (write), commandDownloadProgram (read) — without forcing + // each one to plumb its own timeout. commandPowerOn already accepts its own (longer) + // timeout parameter. Callers needing different limits can override via setReceiveTimeout. + cli_->set_connection_timeout(std::chrono::seconds(5)); + cli_->set_read_timeout(std::chrono::seconds(recv_timeout_.tv_sec) + std::chrono::microseconds(recv_timeout_.tv_usec)); + cli_->set_write_timeout(std::chrono::seconds(send_timeout_.tv_sec) + + std::chrono::microseconds(send_timeout_.tv_usec)); +} + +void DashboardClientImplX::setReceiveTimeout(const timeval& timeout) +{ + recv_timeout_ = timeout; + if (cli_) + { + cli_->set_read_timeout(std::chrono::seconds(timeout.tv_sec) + std::chrono::microseconds(timeout.tv_usec)); + } +} + +void DashboardClientImplX::setSendTimeout(const timeval& timeout) +{ + send_timeout_ = timeout; + if (cli_) + { + cli_->set_write_timeout(std::chrono::seconds(timeout.tv_sec) + std::chrono::microseconds(timeout.tv_usec)); + } } std::string DashboardClientImplX::sendAndReceive([[maybe_unused]] const std::string& text) @@ -91,10 +124,12 @@ void DashboardClientImplX::disconnect() timeval DashboardClientImplX::getConfiguredReceiveTimeout() const { - timeval tv; - tv.tv_sec = 1; - tv.tv_usec = 0; - return tv; + return recv_timeout_; +} + +timeval DashboardClientImplX::getConfiguredSendTimeout() const +{ + return send_timeout_; } VersionInformation DashboardClientImplX::queryPolyScopeVersion() @@ -145,9 +180,35 @@ DashboardResponse DashboardClientImplX::commandPowerOff() return put("/robotstate/v1/state", R"({"action": "POWER_OFF"})"); } -DashboardResponse DashboardClientImplX::commandPowerOn([[maybe_unused]] const std::chrono::duration timeout) +DashboardResponse DashboardClientImplX::commandPowerOn(const std::chrono::duration timeout) { - return put("/robotstate/v1/state", R"({"action": "POWER_ON"})"); + // commandPowerOn can take significantly longer than steady-state dashboard calls (the robot + // boots, runs self-checks, etc.). Bump the read timeout to the caller-supplied value for the + // duration of the PUT, then restore — using the same save-bump-restore pattern as connect(). + // The restore must run on every exit, including exceptions from inside put(), hence the + // catch(...) rethrow guard. + timeval configured_tv = getConfiguredReceiveTimeout(); + // Preserve sub-second precision: duration_cast truncates fractional values, + // so go via microseconds (the smallest unit timeval can represent) and split. + const auto pwron_us = std::chrono::duration_cast(timeout); + timeval pwron_tv; + pwron_tv.tv_sec = static_cast(pwron_us.count() / 1'000'000); + pwron_tv.tv_usec = static_cast(pwron_us.count() % 1'000'000); + setReceiveTimeout(pwron_tv); + + DashboardResponse response; + try + { + response = put("/robotstate/v1/state", R"({"action": "POWER_ON"})"); + } + catch (...) + { + setReceiveTimeout(configured_tv); + throw; + } + + setReceiveTimeout(configured_tv); + return response; } DashboardResponse DashboardClientImplX::commandBrakeRelease() diff --git a/tests/test_dashboard_client_x.cpp b/tests/test_dashboard_client_x.cpp index 089a8accc..b30b02671 100644 --- a/tests/test_dashboard_client_x.cpp +++ b/tests/test_dashboard_client_x.cpp @@ -451,6 +451,58 @@ TEST_F(DashboardClientTestX, download_program) } } +TEST_F(DashboardClientTestX, set_receive_timeout) +{ + timeval expected_tv; + expected_tv.tv_sec = 30; + expected_tv.tv_usec = 0; + dashboard_client_->setReceiveTimeout(expected_tv); + EXPECT_TRUE(dashboard_client_->connect()); + + timeval actual_tv = dashboard_client_->getConfiguredReceiveTimeout(); + EXPECT_EQ(expected_tv.tv_sec, actual_tv.tv_sec); + EXPECT_EQ(expected_tv.tv_usec, actual_tv.tv_usec); +} + +TEST_F(DashboardClientTestX, set_send_timeout) +{ + timeval expected_tv; + expected_tv.tv_sec = 30; + expected_tv.tv_usec = 0; + dashboard_client_->setSendTimeout(expected_tv); + EXPECT_TRUE(dashboard_client_->connect()); + + timeval actual_tv = dashboard_client_->getConfiguredSendTimeout(); + EXPECT_EQ(expected_tv.tv_sec, actual_tv.tv_sec); + EXPECT_EQ(expected_tv.tv_usec, actual_tv.tv_usec); +} + +TEST_F(DashboardClientTestX, timeouts_round_trip_subsecond) +{ + timeval expected_tv; + expected_tv.tv_sec = 7; + expected_tv.tv_usec = 250000; + dashboard_client_->setReceiveTimeout(expected_tv); + dashboard_client_->setSendTimeout(expected_tv); + + timeval recv_tv = dashboard_client_->getConfiguredReceiveTimeout(); + EXPECT_EQ(expected_tv.tv_sec, recv_tv.tv_sec); + EXPECT_EQ(expected_tv.tv_usec, recv_tv.tv_usec); + + timeval send_tv = dashboard_client_->getConfiguredSendTimeout(); + EXPECT_EQ(expected_tv.tv_sec, send_tv.tv_sec); + EXPECT_EQ(expected_tv.tv_usec, send_tv.tv_usec); +} + +TEST_F(DashboardClientTestX, microsecond_receive_timeout_makes_connect_fail) +{ + timeval tiny_tv; + tiny_tv.tv_sec = 0; + tiny_tv.tv_usec = 1; + dashboard_client_->setReceiveTimeout(tiny_tv); + EXPECT_FALSE(dashboard_client_->connect()); +} + int main(int argc, char* argv[]) { ::testing::InitGoogleTest(&argc, argv);