From 1f1b298a5d22d005caec7e24a82423a0f79df1f3 Mon Sep 17 00:00:00 2001 From: "Christopher E. Mower" Date: Wed, 2 Aug 2023 17:24:05 +0100 Subject: [PATCH 01/22] Add pid submodule --- .gitmodules | 3 +++ pid | 1 + 2 files changed, 4 insertions(+) create mode 160000 pid diff --git a/.gitmodules b/.gitmodules index 06bbeb4..aeb2959 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "pybind"] path = pybind url = git@github.com:pybind/pybind11.git +[submodule "pid"] + path = pid + url = https://github.com/cmower/pid diff --git a/pid b/pid new file mode 160000 index 0000000..b6c6c64 --- /dev/null +++ b/pid @@ -0,0 +1 @@ +Subproject commit b6c6c64505d1d0074158cb04c57d8a2178b4837c From ca8656ead2eb7ca9794b68d2331a09ab6ed38862 Mon Sep 17 00:00:00 2001 From: "Christopher E. Mower" Date: Thu, 3 Aug 2023 10:26:04 +0100 Subject: [PATCH 02/22] Incorporate pid library in cmake --- CMakeLists.txt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index c599f50..8e09b47 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -9,10 +9,12 @@ project(_pyFRI) set(CMAKE_POSITION_INDEPENDENT_CODE ON) set(FRI_BUILD_EXAMPLES OFF) +set(PID_BUILD_EXAMPLES OFF) add_subdirectory(pybind) add_subdirectory(FRI-Client-SDK_Cpp) +add_subdirectory(pid) pybind11_add_module(_pyFRI ${CMAKE_CURRENT_SOURCE_DIR}/pyFRI/src/wrapper.cpp) -target_link_libraries(_pyFRI PRIVATE FRIClient) +target_link_libraries(_pyFRI PRIVATE FRIClient pid) From f57df88990db5f872cc4f08fb22fa02109fe9a56 Mon Sep 17 00:00:00 2001 From: "Christopher E. Mower" Date: Thu, 3 Aug 2023 10:30:57 +0100 Subject: [PATCH 03/22] Implement and expose Rate class --- pyFRI/src/wrapper.cpp | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/pyFRI/src/wrapper.cpp b/pyFRI/src/wrapper.cpp index becb98c..c3e6d56 100644 --- a/pyFRI/src/wrapper.cpp +++ b/pyFRI/src/wrapper.cpp @@ -34,6 +34,43 @@ long long getCurrentTimeInNanoseconds() { return duration.count(); } +// +// Rate class +// +// Based on rospy.Rate implementation: +// https://github.com/ros/ros_comm/blob/noetic-devel/clients/rospy/src/rospy/timer.py +// +class Rate { + +public: + Rate(float hz) { + float sleep_dur = 1000000000.0 / hz; + _sleep_dur = static_cast(sleep_dur); + _last_time = getCurrentTimeInNanoseconds(); + } + + void sleep() { + long long curr_time = getCurrentTimeInNanoseconds(); + std::this_thread::sleep_for( + std::chrono::nanoseconds(_remaining(curr_time))); + _last_time += _sleep_dur; + if (curr_time - _last_time > _sleep_dur * 2) + _last_time = curr_time; + } + +private: + long long _last_time; + long long _sleep_dur; + + long long _remaining(long long curr_time) { + + if (_last_time > curr_time) + _last_time = curr_time; + + return _sleep_dur - (curr_time - _last_time); + } +}; + // Make LBRClient a Python abstract class class PyLBRClient : public KUKA::FRI::LBRClient { @@ -541,4 +578,6 @@ PYBIND11_MODULE(_pyFRI, m) { .def("collect_data", &PyClientApplication::collect_data) .def("disconnect", &PyClientApplication::disconnect) .def("step", &PyClientApplication::step); + + py::class_(m, "Rate").def(py::init()).def("sleep", &Rate::sleep); } From a8075596d9ee705d2cda088a8aff8a2205a7f96c Mon Sep 17 00:00:00 2001 From: "Christopher E. Mower" Date: Thu, 3 Aug 2023 11:37:31 +0100 Subject: [PATCH 04/22] Add pthread --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 8e09b47..87a6490 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -17,4 +17,4 @@ add_subdirectory(pid) pybind11_add_module(_pyFRI ${CMAKE_CURRENT_SOURCE_DIR}/pyFRI/src/wrapper.cpp) -target_link_libraries(_pyFRI PRIVATE FRIClient pid) +target_link_libraries(_pyFRI PRIVATE FRIClient pid pthread) From 5581985b13a23bb1bbcd8b71ba41610664550195 Mon Sep 17 00:00:00 2001 From: "Christopher E. Mower" Date: Thu, 3 Aug 2023 11:38:06 +0100 Subject: [PATCH 05/22] Implement async client --- pyFRI/src/async_client.cpp | 149 +++++++++++++++++++++++++++++++++++++ 1 file changed, 149 insertions(+) create mode 100644 pyFRI/src/async_client.cpp diff --git a/pyFRI/src/async_client.cpp b/pyFRI/src/async_client.cpp new file mode 100644 index 0000000..323ce20 --- /dev/null +++ b/pyFRI/src/async_client.cpp @@ -0,0 +1,149 @@ +// Standard library +#include + +// KUKA FRI-Client-SDK_Cpp (using version hosted at: +// https://github.com/cmower/FRI-Client-SDK_Cpp) +#include "friClientApplication.h" +#include "friLBRClient.h" +#include "friUdpConnection.h" + +// PID implementation: https://github.com/cmower/pid +#include "pid.hpp" + +class AsyncLBRClient : public KUKA::FRI::LBRClient { + +private: + static const unsigned int NUM_CART_VEC = 6; + std::vector _set_position; + std::vector _set_wrench; + std::vector _set_torque; + + std::vector _proc_position; + std::vector _proc_wrench; + std::vector _proc_torque; + + double _dt; + + bool _pid_position_ready; + bool _pid_wrench_ready; + bool _pid_torque_ready; + + std::unique_ptr _pid_position; + std::unique_ptr _pid_wrench; + std::unique_ptr _pid_torque; + +public: + AsyncLBRClient() + : _pid_position_ready(false), _pid_wrench_ready(false), + _pid_torque_ready(false) {} + + ~AsyncLBRClient() {} + + void init_pid_position(std::vector Kp, std::vector Ki, + std::vector Kd) { + _pid_position_ready = true; + _pid_position = std::make_unique(static_cast(KUKA::FRI::LBRState::NUMBER_OF_JOINTS), Kp, Ki, Kd); + } + + void init_pid_wrench(std::vector Kp, std::vector Ki, + std::vector Kd) { + _pid_wrench_ready = true; + _pid_wrench = std::make_unique(NUM_CART_VEC, Kp, Ki, Kd); + } + + void init_pid_torque(std::vector Kp, std::vector Ki, + std::vector Kd) { + _pid_torque_ready = true; + _pid_torque = std::make_unique(static_cast(KUKA::FRI::LBRState::NUMBER_OF_JOINTS), Kp, Ki, Kd); + } + + void onStateChange(KUKA::FRI::ESessionState oldState, + KUKA::FRI::ESessionState newState) { + KUKA::FRI::LBRClient::onStateChange(oldState, newState); + } + + void monitor() { KUKA::FRI::LBRClient::monitor(); } + + void waitForCommand() { + + _dt = robotState().getSampleTime(); + + const double *p = robotState().getIpoJointPosition(); + _proc_position = + std::vector(p, p + KUKA::FRI::LBRState::NUMBER_OF_JOINTS); + _set_position = + std::vector(p, p + KUKA::FRI::LBRState::NUMBER_OF_JOINTS); + + robotCommand().setJointPosition(_proc_position.data()); + + switch (robotState().getSessionState()) { + + case KUKA::FRI::EClientCommandMode::WRENCH: { + _proc_wrench = std::vector(NUM_CART_VEC, 0.0); + _set_wrench = std::vector(NUM_CART_VEC, 0.0); + robotCommand().setWrench(_proc_wrench.data()); + break; + } + + case KUKA::FRI::EClientCommandMode::TORQUE: { + _proc_torque = std::vector(KUKA::FRI::LBRState::NUMBER_OF_JOINTS, 0.0); + _set_torque = std::vector(KUKA::FRI::LBRState::NUMBER_OF_JOINTS, 0.0); + robotCommand().setTorque(_proc_torque.data()); + break; + } + } + } + + void command() { + + if (_pid_position_ready) { + _proc_position = _pid_position->next(_set_position, _proc_position, _dt); + } else { + std::cout << "Error: PID not setup for position.\n"; + } + robotCommand().setJointPosition(_proc_position.data()); + + switch (robotState().getSessionState()) { + + case KUKA::FRI::EClientCommandMode::WRENCH: { + if (_pid_wrench_ready) { + _proc_wrench = _pid_wrench->next(_set_wrench, _proc_wrench, _dt); + } else { + std::cout << "Error: PID not setup for wrench.\n"; + } + + robotCommand().setWrench(_proc_wrench.data()); + break; + } + + case KUKA::FRI::EClientCommandMode::TORQUE: { + if (_pid_torque_ready) { + _proc_torque = _pid_torque->next(_set_torque, _proc_torque, _dt); + } else { + std::cout << "Error: PID not setup for torque.\n"; + } + + robotCommand().setTorque(_proc_torque.data()); + break; + } + } + } + + std::vector get_proc_position() { return _proc_position; } + + std::vector get_proc_wrench() { return _proc_wrench; } + + std::vector get_proc_torque() { return _proc_torque; } + + std::vector get_set_position() { return _set_position; } + + std::vector get_set_wrench() { return _set_wrench; } + + std::vector get_set_torque() { return _set_torque; } + + void set_position(std::vector position) { _set_position = position; } + + void set_wrench(std::vector wrench) { _set_wrench = wrench; } + + void set_torque(std::vector torque) { _set_torque = torque; } +}; From a36393fa5826ef27ba7bd5f37d156df214dd6a80 Mon Sep 17 00:00:00 2001 From: "Christopher E. Mower" Date: Thu, 3 Aug 2023 11:38:18 +0100 Subject: [PATCH 06/22] Implement async client application and expose to python --- pyFRI/src/async_client_application.cpp | 112 ++++++++++++++++ pyFRI/src/wrapper.cpp | 178 +++++++++++++++++++++++++ 2 files changed, 290 insertions(+) create mode 100644 pyFRI/src/async_client_application.cpp diff --git a/pyFRI/src/async_client_application.cpp b/pyFRI/src/async_client_application.cpp new file mode 100644 index 0000000..116deed --- /dev/null +++ b/pyFRI/src/async_client_application.cpp @@ -0,0 +1,112 @@ +// Standard library +#include + +// KUKA FRI-Client-SDK_Cpp (using version hosted at: +// https://github.com/cmower/FRI-Client-SDK_Cpp) +#include "friClientApplication.h" +#include "friLBRClient.h" +#include "friUdpConnection.h" + +// Local +#include "async_client.cpp" + +// Asynchronous client application implementation +class AsyncClientApplication { + +private: + bool _success; + bool _fri_loop_continue; + std::thread _fri_loop_thread; + AsyncLBRClient &_client; + KUKA::FRI::UdpConnection _connection; + KUKA::FRI::ClientApplication &_app; + + void _spin_fri() { + while (_success && _fri_loop_continue) { + _success = _app.step(); + if (_client.robotState().getSessionState() == + KUKA::FRI::ESessionState::IDLE) { + _success = false; + break; + } + } + } + +public: + AsyncClientApplication() + : _client(*new AsyncLBRClient), + _app(*new KUKA::FRI::ClientApplication(_connection, _client)), + _fri_loop_continue(false) {} + + bool connect(const int port, char *const remoteHost = NULL) { + + // Connect to controller + _success = _app.connect(port, remoteHost); + + // When successfull, start the fri loop + if (_success) + _fri_loop_continue = true; + _fri_loop_thread = std::thread(&AsyncClientApplication::_spin_fri, this); + + return _success; + } + + bool is_ok() { return _success; } + + std::vector get_ipo_position() const { + const double *p = _client.robotState().getIpoJointPosition(); + return std::vector(p, p + KUKA::FRI::LBRState::NUMBER_OF_JOINTS); + } + + std::vector get_measured_position() const { + const double *p = _client.robotState().getMeasuredJointPosition(); + return std::vector(p, p + KUKA::FRI::LBRState::NUMBER_OF_JOINTS); + } + + std::vector get_measured_torque() const { + const double *t = _client.robotState().getMeasuredTorque(); + return std::vector(t, t + KUKA::FRI::LBRState::NUMBER_OF_JOINTS); + } + + std::vector get_external_torque() const { + const double *t = _client.robotState().getExternalTorque(); + return std::vector(t, t + KUKA::FRI::LBRState::NUMBER_OF_JOINTS); + } + + std::vector get_proc_position() const { + return _client.get_proc_position(); + } + + std::vector get_proc_wrench() const { + return _client.get_proc_wrench(); + } + + std::vector get_proc_torque() const { + return _client.get_proc_torque(); + } + + std::vector get_set_position() const { + return _client.get_set_position(); + } + + std::vector get_set_wrench() const { + return _client.get_set_wrench(); + } + + std::vector get_set_torque() const { + return _client.get_set_torque(); + } + + void set_position(std::vector position) { + _client.set_position(position); + } + + void set_wrench(std::vector wrench) { _client.set_wrench(wrench); } + + void set_torque(std::vector torque) { _client.set_torque(torque); } + + void disconnect() { + _app.disconnect(); + _fri_loop_continue = false; + } +}; diff --git a/pyFRI/src/wrapper.cpp b/pyFRI/src/wrapper.cpp index c3e6d56..6285361 100644 --- a/pyFRI/src/wrapper.cpp +++ b/pyFRI/src/wrapper.cpp @@ -20,6 +20,9 @@ #include "friUdpConnection.h" #include "fri_config.h" +// Local +#include "async_client_application.cpp" + // Function for returning the current time long long getCurrentTimeInNanoseconds() { using namespace std::chrono; @@ -580,4 +583,179 @@ PYBIND11_MODULE(_pyFRI, m) { .def("step", &PyClientApplication::step); py::class_(m, "Rate").def(py::init()).def("sleep", &Rate::sleep); + + py::class_(m, "AsyncClientApplication") + .def(py::init([]() { + auto app = + new AsyncClientApplication(); // Create a new instance using new + std::unique_ptr ptr( + app); // Wrap it in a unique_ptr + return ptr; // Return the unique_ptr + })) + .def("connect", &AsyncClientApplication::connect) + .def("is_ok", &AsyncClientApplication::is_ok) + .def("get_ipo_position", + [](const AsyncClientApplication &self) { + float dataf[KUKA::FRI::LBRState::NUMBER_OF_JOINTS]; + std::vector data = self.get_ipo_position(); + + // Parse: double -> float + for (int i = 0; i < KUKA::FRI::LBRState::NUMBER_OF_JOINTS; i++) + dataf[i] = (float)data[i]; + + return py::array_t({KUKA::FRI::LBRState::NUMBER_OF_JOINTS}, + dataf); + }) + .def("get_measured_position", + [](const AsyncClientApplication &self) { + float dataf[KUKA::FRI::LBRState::NUMBER_OF_JOINTS]; + std::vector data = self.get_measured_position(); + + // Parse: double -> float + for (int i = 0; i < KUKA::FRI::LBRState::NUMBER_OF_JOINTS; i++) + dataf[i] = (float)data[i]; + + return py::array_t({KUKA::FRI::LBRState::NUMBER_OF_JOINTS}, + dataf); + }) + .def("get_measured_torque", + [](const AsyncClientApplication &self) { + float dataf[KUKA::FRI::LBRState::NUMBER_OF_JOINTS]; + std::vector data = self.get_measured_torque(); + + // Parse: double -> float + for (int i = 0; i < KUKA::FRI::LBRState::NUMBER_OF_JOINTS; i++) + dataf[i] = (float)data[i]; + + return py::array_t({KUKA::FRI::LBRState::NUMBER_OF_JOINTS}, + dataf); + }) + .def("get_external_torque", + [](const AsyncClientApplication &self) { + float dataf[KUKA::FRI::LBRState::NUMBER_OF_JOINTS]; + std::vector data = self.get_external_torque(); + + // Parse: double -> float + for (int i = 0; i < KUKA::FRI::LBRState::NUMBER_OF_JOINTS; i++) + dataf[i] = (float)data[i]; + + return py::array_t({KUKA::FRI::LBRState::NUMBER_OF_JOINTS}, + dataf); + }) + .def("get_proc_position", + [](const AsyncClientApplication &self) { + float dataf[KUKA::FRI::LBRState::NUMBER_OF_JOINTS]; + std::vector data = self.get_proc_position(); + + // Parse: double -> float + for (int i = 0; i < KUKA::FRI::LBRState::NUMBER_OF_JOINTS; i++) + dataf[i] = (float)data[i]; + + return py::array_t({KUKA::FRI::LBRState::NUMBER_OF_JOINTS}, + dataf); + }) + .def("get_proc_wrench", + [](const AsyncClientApplication &self) { + float dataf[6]; // 6 is dimension of cartesian vector + std::vector data = self.get_proc_wrench(); + + // Parse: double -> float + for (int i = 0; i < 6; i++) + dataf[i] = (float)data[i]; + + return py::array_t({6}, dataf); + }) + .def("get_proc_torque", + [](const AsyncClientApplication &self) { + float dataf[KUKA::FRI::LBRState::NUMBER_OF_JOINTS]; + std::vector data = self.get_proc_torque(); + + // Parse: double -> float + for (int i = 0; i < KUKA::FRI::LBRState::NUMBER_OF_JOINTS; i++) + dataf[i] = (float)data[i]; + + return py::array_t({KUKA::FRI::LBRState::NUMBER_OF_JOINTS}, + dataf); + }) + .def("get_set_position", + [](const AsyncClientApplication &self) { + float dataf[KUKA::FRI::LBRState::NUMBER_OF_JOINTS]; + std::vector data = self.get_set_position(); + + // Parse: double -> float + for (int i = 0; i < KUKA::FRI::LBRState::NUMBER_OF_JOINTS; i++) + dataf[i] = (float)data[i]; + + return py::array_t({KUKA::FRI::LBRState::NUMBER_OF_JOINTS}, + dataf); + }) + .def("get_set_wrench", + [](const AsyncClientApplication &self) { + float dataf[6]; // 6 is dimension of cartesian vector + std::vector data = self.get_set_wrench(); + + // Parse: double -> float + for (int i = 0; i < 6; i++) + dataf[i] = (float)data[i]; + + return py::array_t({6}, dataf); + }) + .def("get_set_torque", + [](const AsyncClientApplication &self) { + float dataf[KUKA::FRI::LBRState::NUMBER_OF_JOINTS]; + std::vector data = self.get_set_torque(); + + // Parse: double -> float + for (int i = 0; i < KUKA::FRI::LBRState::NUMBER_OF_JOINTS; i++) + dataf[i] = (float)data[i]; + + return py::array_t({KUKA::FRI::LBRState::NUMBER_OF_JOINTS}, + dataf); + }) + .def("set_joint_position", + [](AsyncClientApplication &self, py::array_t values) { + if (values.ndim() != 1 || + PyArray_DIMS(values.ptr())[0] != + KUKA::FRI::LBRState::NUMBER_OF_JOINTS) { + throw std::runtime_error( + "Input array must have shape (" + + std::to_string(KUKA::FRI::LBRState::NUMBER_OF_JOINTS) + + ",)!"); + } + auto buf = values.request(); + const double *data = static_cast(buf.ptr); + std::vector datav; + for (unsigned int i = 0; i < KUKA::FRI::LBRState::NUMBER_OF_JOINTS; + ++i) + datav.push_back(data[i]); + self.set_position(datav); + }) + .def("set_wrench", + [](AsyncClientApplication &self, py::array_t values) { + // 6 is dimension of cartesian vector + if (values.ndim() != 1 || PyArray_DIMS(values.ptr())[0] != 6) { + throw std::runtime_error("Input array must have shape (6,)!"); + } + auto buf = values.request(); + const double *data = static_cast(buf.ptr); + std::vector datav; + for (unsigned int i = 0; i < 6; ++i) + datav.push_back(data[i]); + self.set_wrench(datav); + }) + .def("set_torque", [](AsyncClientApplication &self, + py::array_t values) { + if (values.ndim() != 1 || PyArray_DIMS(values.ptr())[0] != + KUKA::FRI::LBRState::NUMBER_OF_JOINTS) { + throw std::runtime_error( + "Input array must have shape (" + + std::to_string(KUKA::FRI::LBRState::NUMBER_OF_JOINTS) + ",)!"); + } + auto buf = values.request(); + const double *data = static_cast(buf.ptr); + std::vector datav; + for (unsigned int i = 0; i < KUKA::FRI::LBRState::NUMBER_OF_JOINTS; ++i) + datav.push_back(data[i]); + self.set_torque(datav); + }); } From e4be059963fb4ce1f6b791c467fc5bafac36c98a Mon Sep 17 00:00:00 2001 From: "Christopher E. Mower" Date: Thu, 3 Aug 2023 11:38:30 +0100 Subject: [PATCH 07/22] Add async example script --- examples/async_example.py | 108 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 examples/async_example.py diff --git a/examples/async_example.py b/examples/async_example.py new file mode 100644 index 0000000..63c2e1f --- /dev/null +++ b/examples/async_example.py @@ -0,0 +1,108 @@ +import time +import math +import argparse +import pyFRI as fri + +def get_arguments(): + def cvt_joint_mask(value): + int_value = int(value) + if 0 <= int_value < 7: + return int_value + else: + raise argparse.ArgumentTypeError(f"{value} is not in the range [0, 7).") + + parser = argparse.ArgumentParser(description="LRBJointSineOverlay example.") + parser.add_argument( + "--hostname", + dest="hostname", + default=None, + help="The hostname used to communicate with the KUKA Sunrise Controller.", + ) + parser.add_argument( + "--port", + dest="port", + type=int, + default=30200, + help="The port number used to communicate with the KUKA Sunrise Controller.", + ) + parser.add_argument( + "--joint-mask", + dest="joint_mask", + type=cvt_joint_mask, + default=3, + help="The joint to move.", + ) + parser.add_argument( + "--freq-hz", + dest="freq_hz", + type=float, + default=0.25, + help="The frequency of the sine wave.", + ) + parser.add_argument( + "--ampl-rad", + dest="ampl_rad", + type=float, + default=0.04, + help="Applitude of the sine wave.", + ) + parser.add_argument( + "--filter-coeff", + dest="filter_coeff", + type=float, + default=0.99, + help="Exponential smoothing coeficient.", + ) + parser.add_argument( + "--save-data", + dest="save_data", + action="store_true", + default=False, + help="Set this flag to save the data.", + ) + + return parser.parse_args() + +def main(): + print("Running FRI Version:", fri.FRI_VERSION) + + args = get_arguments() + + app = fri.AsyncClientApplication() + success = app.connect(args.port, args.hostname) + + time.sleep(0.5) # wait to ensure fri loop started + + hz = 50 + time_step = 1./float(hz) + + q = app.get_proc_position() + offset = 0.0 + phi = 0.0 + step_width = 2 * math.pi * args.freq_hz * time_step + + if not success: + print("Connection to KUKA Sunrise controller failed.") + return 1 + + try: + rate = fri.Rate(hz) + while app.is_ok(): + new_offset = args.ampl_rad * math.sin(phi) + offset = (offset * args.filter_coeff) + ( + new_offset * (1.0 - args.filter_coeff) + ) + phi += step_width + if phi >= (2 * math.pi): + phi -= 2 * math.pi + q[args.joint_mask] += offset + app.set_position(q.astype(np.float32)) + rate.sleep() + except KeyboardInterrupt: + pass + finally: + app.disconnect() + + +if __name__ == '__main__': + main() From 6dd2b4faff18e30fc72b67f5c8991310e35e130e Mon Sep 17 00:00:00 2001 From: "Christopher E. Mower" Date: Thu, 3 Aug 2023 11:54:48 +0100 Subject: [PATCH 08/22] Ensure PID is initialized --- pyFRI/src/async_client_application.cpp | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/pyFRI/src/async_client_application.cpp b/pyFRI/src/async_client_application.cpp index 116deed..f1356d5 100644 --- a/pyFRI/src/async_client_application.cpp +++ b/pyFRI/src/async_client_application.cpp @@ -36,7 +36,27 @@ class AsyncClientApplication { AsyncClientApplication() : _client(*new AsyncLBRClient), _app(*new KUKA::FRI::ClientApplication(_connection, _client)), - _fri_loop_continue(false) {} + _fri_loop_continue(false) { + + std::vector Kp_position = {1., 1., 1., 1., 1., 1., 1.}; + std::vector Ki_position = + std::vector(KUKA::FRI::LBRState::NUMBER_OF_JOINTS, 0.0); + std::vector Kd_position = + std::vector(KUKA::FRI::LBRState::NUMBER_OF_JOINTS, 0.0); + _client.init_pid_position(Kp_position, Ki_position, Kd_position); + + std::vector Kp_wrench = {1., 1., 1., 1., 1., 1.}; + std::vector Ki_wrench = std::vector(6, 0.0); + std::vector Kd_wrench = std::vector(6, 0.0); + _client.init_pid_wrench(Kp_wrench, Ki_wrench, Kd_wrench); + + std::vector Kp_torque = {1., 1., 1., 1., 1., 1., 1.}; + std::vector Ki_torque = + std::vector(KUKA::FRI::LBRState::NUMBER_OF_JOINTS, 0.0); + std::vector Kd_torque = + std::vector(KUKA::FRI::LBRState::NUMBER_OF_JOINTS, 0.0); + _client.init_pid_torque(Kp_torque, Ki_torque, Kd_torque); + } bool connect(const int port, char *const remoteHost = NULL) { From 302f41d4f1588b7dedc72062ede6e63a0dc7219e Mon Sep 17 00:00:00 2001 From: "Christopher E. Mower" Date: Thu, 3 Aug 2023 13:00:08 +0100 Subject: [PATCH 09/22] Bugfixes and updates to example --- examples/async_example.py | 29 ++++++++++++++++++-------- pyFRI/src/async_client.cpp | 17 ++++++++++----- pyFRI/src/async_client_application.cpp | 14 ++++++++++--- pyFRI/src/wrapper.cpp | 20 ++++++++++-------- 4 files changed, 54 insertions(+), 26 deletions(-) diff --git a/examples/async_example.py b/examples/async_example.py index 63c2e1f..0559de2 100644 --- a/examples/async_example.py +++ b/examples/async_example.py @@ -3,6 +3,7 @@ import argparse import pyFRI as fri + def get_arguments(): def cvt_joint_mask(value): int_value = int(value) @@ -63,28 +64,38 @@ def cvt_joint_mask(value): return parser.parse_args() + def main(): print("Running FRI Version:", fri.FRI_VERSION) args = get_arguments() app = fri.AsyncClientApplication() - success = app.connect(args.port, args.hostname) + if app.connect(args.port, args.hostname): + print("Connected to KUKA Sunrise controller.") + else: + print("Connection to KUKA Sunrise controller failed.") + return - time.sleep(0.5) # wait to ensure fri loop started + # Wait for FRI loop to start spinning + try: + while not app.is_spinning(): + pass + except KeyboardInterrupt: + pass + finally: + app.disconnect() + print("Goodbye") + return hz = 50 - time_step = 1./float(hz) + time_step = 1.0 / float(hz) q = app.get_proc_position() offset = 0.0 phi = 0.0 step_width = 2 * math.pi * args.freq_hz * time_step - if not success: - print("Connection to KUKA Sunrise controller failed.") - return 1 - try: rate = fri.Rate(hz) while app.is_ok(): @@ -102,7 +113,7 @@ def main(): pass finally: app.disconnect() - -if __name__ == '__main__': + +if __name__ == "__main__": main() diff --git a/pyFRI/src/async_client.cpp b/pyFRI/src/async_client.cpp index 323ce20..603b808 100644 --- a/pyFRI/src/async_client.cpp +++ b/pyFRI/src/async_client.cpp @@ -10,10 +10,11 @@ // PID implementation: https://github.com/cmower/pid #include "pid.hpp" +const unsigned int NUM_CART_VEC = 6; + class AsyncLBRClient : public KUKA::FRI::LBRClient { private: - static const unsigned int NUM_CART_VEC = 6; std::vector _set_position; std::vector _set_wrench; std::vector _set_torque; @@ -42,7 +43,9 @@ class AsyncLBRClient : public KUKA::FRI::LBRClient { void init_pid_position(std::vector Kp, std::vector Ki, std::vector Kd) { _pid_position_ready = true; - _pid_position = std::make_unique(static_cast(KUKA::FRI::LBRState::NUMBER_OF_JOINTS), Kp, Ki, Kd); + _pid_position = std::make_unique( + static_cast(KUKA::FRI::LBRState::NUMBER_OF_JOINTS), Kp, + Ki, Kd); } void init_pid_wrench(std::vector Kp, std::vector Ki, @@ -54,7 +57,9 @@ class AsyncLBRClient : public KUKA::FRI::LBRClient { void init_pid_torque(std::vector Kp, std::vector Ki, std::vector Kd) { _pid_torque_ready = true; - _pid_torque = std::make_unique(static_cast(KUKA::FRI::LBRState::NUMBER_OF_JOINTS), Kp, Ki, Kd); + _pid_torque = std::make_unique( + static_cast(KUKA::FRI::LBRState::NUMBER_OF_JOINTS), Kp, + Ki, Kd); } void onStateChange(KUKA::FRI::ESessionState oldState, @@ -86,8 +91,10 @@ class AsyncLBRClient : public KUKA::FRI::LBRClient { } case KUKA::FRI::EClientCommandMode::TORQUE: { - _proc_torque = std::vector(KUKA::FRI::LBRState::NUMBER_OF_JOINTS, 0.0); - _set_torque = std::vector(KUKA::FRI::LBRState::NUMBER_OF_JOINTS, 0.0); + _proc_torque = + std::vector(KUKA::FRI::LBRState::NUMBER_OF_JOINTS, 0.0); + _set_torque = + std::vector(KUKA::FRI::LBRState::NUMBER_OF_JOINTS, 0.0); robotCommand().setTorque(_proc_torque.data()); break; } diff --git a/pyFRI/src/async_client_application.cpp b/pyFRI/src/async_client_application.cpp index f1356d5..e4c59f1 100644 --- a/pyFRI/src/async_client_application.cpp +++ b/pyFRI/src/async_client_application.cpp @@ -16,6 +16,7 @@ class AsyncClientApplication { private: bool _success; bool _fri_loop_continue; + bool _spinning; std::thread _fri_loop_thread; AsyncLBRClient &_client; KUKA::FRI::UdpConnection _connection; @@ -24,9 +25,14 @@ class AsyncClientApplication { void _spin_fri() { while (_success && _fri_loop_continue) { _success = _app.step(); + + if (_success) + _spinning = true; + if (_client.robotState().getSessionState() == KUKA::FRI::ESessionState::IDLE) { _success = false; + _spinning = false; break; } } @@ -36,7 +42,7 @@ class AsyncClientApplication { AsyncClientApplication() : _client(*new AsyncLBRClient), _app(*new KUKA::FRI::ClientApplication(_connection, _client)), - _fri_loop_continue(false) { + _fri_loop_continue(false), _spinning(false) { std::vector Kp_position = {1., 1., 1., 1., 1., 1., 1.}; std::vector Ki_position = @@ -46,8 +52,8 @@ class AsyncClientApplication { _client.init_pid_position(Kp_position, Ki_position, Kd_position); std::vector Kp_wrench = {1., 1., 1., 1., 1., 1.}; - std::vector Ki_wrench = std::vector(6, 0.0); - std::vector Kd_wrench = std::vector(6, 0.0); + std::vector Ki_wrench = std::vector(NUM_CART_VEC, 0.0); + std::vector Kd_wrench = std::vector(NUM_CART_VEC, 0.0); _client.init_pid_wrench(Kp_wrench, Ki_wrench, Kd_wrench); std::vector Kp_torque = {1., 1., 1., 1., 1., 1., 1.}; @@ -71,6 +77,8 @@ class AsyncClientApplication { return _success; } + bool is_spinning() { return _spinning; } + bool is_ok() { return _success; } std::vector get_ipo_position() const { diff --git a/pyFRI/src/wrapper.cpp b/pyFRI/src/wrapper.cpp index 6285361..ce06bc5 100644 --- a/pyFRI/src/wrapper.cpp +++ b/pyFRI/src/wrapper.cpp @@ -593,7 +593,9 @@ PYBIND11_MODULE(_pyFRI, m) { return ptr; // Return the unique_ptr })) .def("connect", &AsyncClientApplication::connect) + .def("disconnect", &AsyncClientApplication::disconnect) .def("is_ok", &AsyncClientApplication::is_ok) + .def("is_spinning", &AsyncClientApplication::is_spinning) .def("get_ipo_position", [](const AsyncClientApplication &self) { float dataf[KUKA::FRI::LBRState::NUMBER_OF_JOINTS]; @@ -656,14 +658,14 @@ PYBIND11_MODULE(_pyFRI, m) { }) .def("get_proc_wrench", [](const AsyncClientApplication &self) { - float dataf[6]; // 6 is dimension of cartesian vector + float dataf[NUM_CART_VEC]; std::vector data = self.get_proc_wrench(); // Parse: double -> float - for (int i = 0; i < 6; i++) + for (int i = 0; i < NUM_CART_VEC; i++) dataf[i] = (float)data[i]; - return py::array_t({6}, dataf); + return py::array_t({NUM_CART_VEC}, dataf); }) .def("get_proc_torque", [](const AsyncClientApplication &self) { @@ -691,14 +693,14 @@ PYBIND11_MODULE(_pyFRI, m) { }) .def("get_set_wrench", [](const AsyncClientApplication &self) { - float dataf[6]; // 6 is dimension of cartesian vector + float dataf[NUM_CART_VEC]; std::vector data = self.get_set_wrench(); // Parse: double -> float - for (int i = 0; i < 6; i++) + for (int i = 0; i < NUM_CART_VEC; i++) dataf[i] = (float)data[i]; - return py::array_t({6}, dataf); + return py::array_t({NUM_CART_VEC}, dataf); }) .def("get_set_torque", [](const AsyncClientApplication &self) { @@ -732,14 +734,14 @@ PYBIND11_MODULE(_pyFRI, m) { }) .def("set_wrench", [](AsyncClientApplication &self, py::array_t values) { - // 6 is dimension of cartesian vector - if (values.ndim() != 1 || PyArray_DIMS(values.ptr())[0] != 6) { + if (values.ndim() != 1 || + PyArray_DIMS(values.ptr())[0] != NUM_CART_VEC) { throw std::runtime_error("Input array must have shape (6,)!"); } auto buf = values.request(); const double *data = static_cast(buf.ptr); std::vector datav; - for (unsigned int i = 0; i < 6; ++i) + for (unsigned int i = 0; i < NUM_CART_VEC; ++i) datav.push_back(data[i]); self.set_wrench(datav); }) From 2ba8d8aa077acd3d91982c2b046ed25491126a49 Mon Sep 17 00:00:00 2001 From: "Christopher E. Mower" Date: Thu, 3 Aug 2023 13:04:54 +0100 Subject: [PATCH 10/22] Minor update to waiting scheme --- examples/async_example.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/examples/async_example.py b/examples/async_example.py index 0559de2..7068e25 100644 --- a/examples/async_example.py +++ b/examples/async_example.py @@ -79,8 +79,16 @@ def main(): # Wait for FRI loop to start spinning try: + rate_wait = fri.Rate(1) + counter = 1 + max_counter = 5 while not app.is_spinning(): - pass + print("Waiting for FRI loop to start, attempt", counter, "of", max_counter) + counter += 1 + if counter == max_counter + 1: + print("FRI loop did not start, quitting") + return + rate_wait.sleep() except KeyboardInterrupt: pass finally: From e266c617d01b93dd5218713cee082395271a207e Mon Sep 17 00:00:00 2001 From: "Christopher E. Mower" Date: Thu, 3 Aug 2023 13:13:08 +0100 Subject: [PATCH 11/22] Update submodule to latest commit --- pid | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pid b/pid index b6c6c64..6b44a60 160000 --- a/pid +++ b/pid @@ -1 +1 @@ -Subproject commit b6c6c64505d1d0074158cb04c57d8a2178b4837c +Subproject commit 6b44a60de1c36b2e8d4be2272d8c8c622b468f6f From 7c3176f4423afdeb01b2417de31e2ff79c76258f Mon Sep 17 00:00:00 2001 From: "Christopher E. Mower" Date: Thu, 3 Aug 2023 13:15:38 +0100 Subject: [PATCH 12/22] Update submodule to latest commit and update async client --- pid | 2 +- pyFRI/src/async_client.cpp | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pid b/pid index 6b44a60..a930448 160000 --- a/pid +++ b/pid @@ -1 +1 @@ -Subproject commit 6b44a60de1c36b2e8d4be2272d8c8c622b468f6f +Subproject commit a930448baf4a434f0456b1e270dd37bf754165c1 diff --git a/pyFRI/src/async_client.cpp b/pyFRI/src/async_client.cpp index 603b808..e34f80b 100644 --- a/pyFRI/src/async_client.cpp +++ b/pyFRI/src/async_client.cpp @@ -29,9 +29,9 @@ class AsyncLBRClient : public KUKA::FRI::LBRClient { bool _pid_wrench_ready; bool _pid_torque_ready; - std::unique_ptr _pid_position; - std::unique_ptr _pid_wrench; - std::unique_ptr _pid_torque; + std::unique_ptr _pid_position; + std::unique_ptr _pid_wrench; + std::unique_ptr _pid_torque; public: AsyncLBRClient() @@ -43,7 +43,7 @@ class AsyncLBRClient : public KUKA::FRI::LBRClient { void init_pid_position(std::vector Kp, std::vector Ki, std::vector Kd) { _pid_position_ready = true; - _pid_position = std::make_unique( + _pid_position = std::make_unique( static_cast(KUKA::FRI::LBRState::NUMBER_OF_JOINTS), Kp, Ki, Kd); } @@ -51,13 +51,13 @@ class AsyncLBRClient : public KUKA::FRI::LBRClient { void init_pid_wrench(std::vector Kp, std::vector Ki, std::vector Kd) { _pid_wrench_ready = true; - _pid_wrench = std::make_unique(NUM_CART_VEC, Kp, Ki, Kd); + _pid_wrench = std::make_unique(NUM_CART_VEC, Kp, Ki, Kd); } void init_pid_torque(std::vector Kp, std::vector Ki, std::vector Kd) { _pid_torque_ready = true; - _pid_torque = std::make_unique( + _pid_torque = std::make_unique( static_cast(KUKA::FRI::LBRState::NUMBER_OF_JOINTS), Kp, Ki, Kd); } From 1edd294b98972128f87970100d08a1ad087ac892 Mon Sep 17 00:00:00 2001 From: "Christopher E. Mower" Date: Fri, 4 Aug 2023 15:21:31 +0100 Subject: [PATCH 13/22] Bugfixes and some changes (still WIP) --- examples/async_example.py | 51 ++++++++++++++++++++------------------- 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/examples/async_example.py b/examples/async_example.py index 7068e25..6086e6f 100644 --- a/examples/async_example.py +++ b/examples/async_example.py @@ -1,6 +1,7 @@ import time import math import argparse +import numpy as np import pyFRI as fri @@ -54,13 +55,6 @@ def cvt_joint_mask(value): default=0.99, help="Exponential smoothing coeficient.", ) - parser.add_argument( - "--save-data", - dest="save_data", - action="store_true", - default=False, - help="Set this flag to save the data.", - ) return parser.parse_args() @@ -78,23 +72,30 @@ def main(): return # Wait for FRI loop to start spinning - try: - rate_wait = fri.Rate(1) - counter = 1 - max_counter = 5 - while not app.is_spinning(): - print("Waiting for FRI loop to start, attempt", counter, "of", max_counter) - counter += 1 - if counter == max_counter + 1: - print("FRI loop did not start, quitting") - return - rate_wait.sleep() - except KeyboardInterrupt: - pass - finally: - app.disconnect() - print("Goodbye") - return + # try: + rate_wait = fri.Rate(1) + counter = 1 + max_counter = 5 + spinning = app.is_spinning() + print("spinning=", spinning) + while not spinning: + print("Waiting for FRI loop to start, attempt", counter, "of", max_counter) + counter += 1 + if counter == max_counter + 1: + print("FRI loop did not start, quitting") + return + rate_wait.sleep() + spinning = app.is_spinning() + print("spinning=", spinning) + time.sleep(10.) + # except KeyboardInterrupt: + # pass + # finally: + # app.disconnect() + # print("Goodbye for now") + # return + + print("FRI Loop started") hz = 50 time_step = 1.0 / float(hz) @@ -115,7 +116,7 @@ def main(): if phi >= (2 * math.pi): phi -= 2 * math.pi q[args.joint_mask] += offset - app.set_position(q.astype(np.float32)) + app.set_joint_position(q.astype(np.float32)) rate.sleep() except KeyboardInterrupt: pass From 8fc2f10372e9db1f40ddb2450cf2cf276cf6eea2 Mon Sep 17 00:00:00 2001 From: "Christopher E. Mower" Date: Fri, 4 Aug 2023 16:53:02 +0100 Subject: [PATCH 14/22] Async working (implementation is still a work in process) --- examples/async_example.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/examples/async_example.py b/examples/async_example.py index 6086e6f..7619036 100644 --- a/examples/async_example.py +++ b/examples/async_example.py @@ -4,6 +4,8 @@ import numpy as np import pyFRI as fri +np.set_printoptions(precision=3, suppress=True, linewidth=1000) + def get_arguments(): def cvt_joint_mask(value): @@ -97,16 +99,17 @@ def main(): print("FRI Loop started") - hz = 50 + hz = 10 time_step = 1.0 / float(hz) - q = app.get_proc_position() + q0 = app.get_proc_position() offset = 0.0 phi = 0.0 step_width = 2 * math.pi * args.freq_hz * time_step try: rate = fri.Rate(hz) + t = 0. while app.is_ok(): new_offset = args.ampl_rad * math.sin(phi) offset = (offset * args.filter_coeff) + ( @@ -115,9 +118,15 @@ def main(): phi += step_width if phi >= (2 * math.pi): phi -= 2 * math.pi - q[args.joint_mask] += offset + # q[args.joint_mask] += offset + q = q0.copy() + q[args.joint_mask] += math.radians(20)*math.sin(t*0.) + # print(q) app.set_joint_position(q.astype(np.float32)) + qm = app.get_measured_position() + print(np.linalg.norm(qm - q)) rate.sleep() + t += time_step except KeyboardInterrupt: pass finally: From 25395c28240a0be5c671f53fd4099296ff4b11e5 Mon Sep 17 00:00:00 2001 From: "Christopher E. Mower" Date: Tue, 8 Aug 2023 14:27:40 +0100 Subject: [PATCH 15/22] Update submodule remote location and upgrade to latest commit --- .gitmodules | 2 +- pid | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitmodules b/.gitmodules index aeb2959..4a418c8 100644 --- a/.gitmodules +++ b/.gitmodules @@ -6,4 +6,4 @@ url = git@github.com:pybind/pybind11.git [submodule "pid"] path = pid - url = https://github.com/cmower/pid + url = git@github.com:lbr-stack/pid.git diff --git a/pid b/pid index a930448..64c2c7e 160000 --- a/pid +++ b/pid @@ -1 +1 @@ -Subproject commit a930448baf4a434f0456b1e270dd37bf754165c1 +Subproject commit 64c2c7e7a4b62bd54b27c8d97a8e41f7518f5be4 From fd88c473d51040647ebdfba37bc5535967155982 Mon Sep 17 00:00:00 2001 From: "Christopher E. Mower" Date: Tue, 8 Aug 2023 18:15:39 +0100 Subject: [PATCH 16/22] Update submodule to latest commit --- pid | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pid b/pid index 64c2c7e..ac2e11e 160000 --- a/pid +++ b/pid @@ -1 +1 @@ -Subproject commit 64c2c7e7a4b62bd54b27c8d97a8e41f7518f5be4 +Subproject commit ac2e11e684de5fe54aa88e5a7cced81796b43dac From e2049e3b81cf6947d65d7c436c1546fb26dda32d Mon Sep 17 00:00:00 2001 From: "Christopher E. Mower" Date: Tue, 8 Aug 2023 18:15:56 +0100 Subject: [PATCH 17/22] Revamp async --- pyFRI/src/async_client.cpp | 258 +++++++++++++------- pyFRI/src/async_client_application.cpp | 132 +++------- pyFRI/src/wrapper.cpp | 317 ++++++------------------- 3 files changed, 282 insertions(+), 425 deletions(-) diff --git a/pyFRI/src/async_client.cpp b/pyFRI/src/async_client.cpp index e34f80b..7933479 100644 --- a/pyFRI/src/async_client.cpp +++ b/pyFRI/src/async_client.cpp @@ -1,5 +1,6 @@ // Standard library #include +#include // KUKA FRI-Client-SDK_Cpp (using version hosted at: // https://github.com/cmower/FRI-Client-SDK_Cpp) @@ -10,147 +11,224 @@ // PID implementation: https://github.com/cmower/pid #include "pid.hpp" -const unsigned int NUM_CART_VEC = 6; +const unsigned int NCART = 6; // number of dimensions in cartesian vector +const unsigned int NDOF = + KUKA::FRI::LBRState::NUMBER_OF_JOINTS; // number of degrees of freedom class AsyncLBRClient : public KUKA::FRI::LBRClient { private: - std::vector _set_position; - std::vector _set_wrench; - std::vector _set_torque; + bool _ready; - std::vector _proc_position; - std::vector _proc_wrench; - std::vector _proc_torque; + KUKA::FRI::EClientCommandMode _ccmode; double _dt; - bool _pid_position_ready; - bool _pid_wrench_ready; - bool _pid_torque_ready; + double _set_position[NDOF]; + double _set_wrench[NCART]; + double _set_torque[NDOF]; - std::unique_ptr _pid_position; - std::unique_ptr _pid_wrench; - std::unique_ptr _pid_torque; + double _pv_position[NDOF]; + double _pv_wrench[NCART]; + double _pv_torque[NDOF]; -public: - AsyncLBRClient() - : _pid_position_ready(false), _pid_wrench_ready(false), - _pid_torque_ready(false) {} - - ~AsyncLBRClient() {} + PIDControl::PID _pid_position[NDOF]; + PIDControl::PID _pid_wrench[NCART]; + PIDControl::PID _pid_torque[NDOF]; - void init_pid_position(std::vector Kp, std::vector Ki, - std::vector Kd) { - _pid_position_ready = true; - _pid_position = std::make_unique( - static_cast(KUKA::FRI::LBRState::NUMBER_OF_JOINTS), Kp, - Ki, Kd); + bool _is_position_pid_ready() { + for (auto &pid : _pid_position) { + if (!pid.is_ready()) + return false; + } + return true; } - void init_pid_wrench(std::vector Kp, std::vector Ki, - std::vector Kd) { - _pid_wrench_ready = true; - _pid_wrench = std::make_unique(NUM_CART_VEC, Kp, Ki, Kd); + bool _is_wrench_pid_ready() { + for (auto &pid : _pid_wrench) { + if (!pid.is_ready()) + return false; + } + return true; } - void init_pid_torque(std::vector Kp, std::vector Ki, - std::vector Kd) { - _pid_torque_ready = true; - _pid_torque = std::make_unique( - static_cast(KUKA::FRI::LBRState::NUMBER_OF_JOINTS), Kp, - Ki, Kd); + bool _is_torque_pid_ready() { + for (auto &pid : _pid_torque) { + if (!pid.is_ready()) + return false; + } + return true; } - void onStateChange(KUKA::FRI::ESessionState oldState, - KUKA::FRI::ESessionState newState) { - KUKA::FRI::LBRClient::onStateChange(oldState, newState); - } + void _command() { - void monitor() { KUKA::FRI::LBRClient::monitor(); } + // Command position + robotCommand().setJointPosition(_pv_position); - void waitForCommand() { + // Command wrench/torque + switch (_ccmode) { - _dt = robotState().getSampleTime(); + case KUKA::FRI::EClientCommandMode::WRENCH: { + robotCommand().setWrench(_pv_wrench); + break; + } - const double *p = robotState().getIpoJointPosition(); - _proc_position = - std::vector(p, p + KUKA::FRI::LBRState::NUMBER_OF_JOINTS); - _set_position = - std::vector(p, p + KUKA::FRI::LBRState::NUMBER_OF_JOINTS); + case KUKA::FRI::EClientCommandMode::TORQUE: { + robotCommand().setTorque(_pv_torque); + break; + } + } + } + + void _update() { - robotCommand().setJointPosition(_proc_position.data()); + // When not ready simply return + if (!_ready) + return; - switch (robotState().getSessionState()) { + // Update PID + for (unsigned int i = 0; i < NDOF; ++i) + _pv_position[i] = + _pid_position[i].next(_set_position[i], _pv_position[i], _dt); + + switch (_ccmode) { case KUKA::FRI::EClientCommandMode::WRENCH: { - _proc_wrench = std::vector(NUM_CART_VEC, 0.0); - _set_wrench = std::vector(NUM_CART_VEC, 0.0); - robotCommand().setWrench(_proc_wrench.data()); + for (unsigned int i = 0; i < NCART; ++i) + _pv_wrench[i] = _pid_wrench[i].next(_set_wrench[i], _pv_wrench[i], _dt); break; } case KUKA::FRI::EClientCommandMode::TORQUE: { - _proc_torque = - std::vector(KUKA::FRI::LBRState::NUMBER_OF_JOINTS, 0.0); - _set_torque = - std::vector(KUKA::FRI::LBRState::NUMBER_OF_JOINTS, 0.0); - robotCommand().setTorque(_proc_torque.data()); + for (unsigned int i = 0; i < NDOF; ++i) + _pv_torque[i] = _pid_torque[i].next(_set_torque[i], _pv_torque[i], _dt); break; } } } - void command() { +public: + AsyncLBRClient() : _ready(false) { + + // Fill set/pv position and torque with zeros + for (unsigned i = 0; i < NDOF; ++i) { + _set_position[i] = 0.0; + _pv_position[i] = 0.0; + _set_torque[i] = 0.0; + _pv_torque[i] = 0.0; + } - if (_pid_position_ready) { - _proc_position = _pid_position->next(_set_position, _proc_position, _dt); - } else { - std::cout << "Error: PID not setup for position.\n"; + // Fill set/pv wrench with zeros + for (unsigned i = 0; i < NCART; ++i) { + _set_wrench[i] = 0.0; + _pv_wrench[i] = 0.0; } - robotCommand().setJointPosition(_proc_position.data()); + } - switch (robotState().getSessionState()) { + ~AsyncLBRClient() {} - case KUKA::FRI::EClientCommandMode::WRENCH: { - if (_pid_wrench_ready) { - _proc_wrench = _pid_wrench->next(_set_wrench, _proc_wrench, _dt); - } else { - std::cout << "Error: PID not setup for wrench.\n"; - } + void set_pid_position_gains(double Kp[NDOF], double Ki[NDOF], + double Kd[NDOF]) { + for (unsigned int i = 0; i < NDOF; ++i) + _pid_position[i].set_gains(Kp[i], Ki[i], Kd[i]); + } - robotCommand().setWrench(_proc_wrench.data()); - break; - } + void set_pid_wrench_gains(double Kp[NCART], double Ki[NCART], + double Kd[NCART]) { + for (unsigned int i = 0; i < NCART; ++i) + _pid_wrench[i].set_gains(Kp[i], Ki[i], Kd[i]); + } - case KUKA::FRI::EClientCommandMode::TORQUE: { - if (_pid_torque_ready) { - _proc_torque = _pid_torque->next(_set_torque, _proc_torque, _dt); - } else { - std::cout << "Error: PID not setup for torque.\n"; - } + void set_pid_torque_gains(double Kp[NDOF], double Ki[NDOF], double Kd[NDOF]) { + for (unsigned int i = 0; i < NDOF; ++i) + _pid_torque[i].set_gains(Kp[i], Ki[i], Kd[i]); + } - robotCommand().setTorque(_proc_torque.data()); - break; + void set_position(double position[NDOF]) { + for (unsigned int i = 0; i < NDOF; ++i) { + _set_position[i] = position[i]; + } + } + + void set_wrench(double wrench[NCART]) { + for (unsigned int i = 0; i < NCART; ++i) { + _set_wrench[i] = wrench[i]; } + } + + void set_torque(double torque[NDOF]) { + for (unsigned int i = 0; i < NDOF; ++i) { + _set_torque[i] = torque[i]; } } - std::vector get_proc_position() { return _proc_position; } + void onStateChange(KUKA::FRI::ESessionState oldState, + KUKA::FRI::ESessionState newState) { + + // Report state change + KUKA::FRI::LBRClient::onStateChange(oldState, newState); - std::vector get_proc_wrench() { return _proc_wrench; } + // Set set/pv position + if (newState == KUKA::FRI::ESessionState::MONITORING_READY) { - std::vector get_proc_torque() { return _proc_torque; } + // Get sample time + _dt = robotState().getSampleTime(); - std::vector get_set_position() { return _set_position; } + // Get client command mode + _ccmode = robotState().getClientCommandMode(); - std::vector get_set_wrench() { return _set_wrench; } + // Retrieve current joint position + memcpy(_pv_position, robotState().getIpoJointPosition(), + NDOF * sizeof(double)); - std::vector get_set_torque() { return _set_torque; } + // Initialize set position + for (unsigned int i = 0; i < NDOF; ++i) { + _set_position[i] = _pv_position[i]; - void set_position(std::vector position) { _set_position = position; } + // Reset position/torque PID + _pid_position[i].reset(); + _pid_torque[i].reset(); + } - void set_wrench(std::vector wrench) { _set_wrench = wrench; } + // Reset wrench PID + for (unsigned int i = 0; i < NCART; ++i) + _pid_wrench[i].reset(); - void set_torque(std::vector torque) { _set_torque = torque; } + // Ensure gains are set for position PID controller + if (!_is_position_pid_ready()) { + std::cout << "Error: you must set gains for PID position controller.\n"; + return; + } + + // Ensure gains are set for wrench/torque PID controllers + switch (_ccmode) { + + case KUKA::FRI::EClientCommandMode::WRENCH: { + if (!_is_wrench_pid_ready()) { + std::cout << "Error: you must set gains for PID wrench controller.\n"; + return; + } + } + + case KUKA::FRI::EClientCommandMode::TORQUE: { + if (!_is_torque_pid_ready()) { + std::cout << "Error: you must set gains for PID torque controller.\n"; + return; + } + } + } + + // Above checks passed -> client is ready + _ready = true; + } + } + + void monitor() { KUKA::FRI::LBRClient::monitor(); } + + void waitForCommand() { _command(); } + + void command() { + _update(); + _command(); + } }; diff --git a/pyFRI/src/async_client_application.cpp b/pyFRI/src/async_client_application.cpp index e4c59f1..466c326 100644 --- a/pyFRI/src/async_client_application.cpp +++ b/pyFRI/src/async_client_application.cpp @@ -14,26 +14,35 @@ class AsyncClientApplication { private: - bool _success; - bool _fri_loop_continue; - bool _spinning; + bool _connected; + bool _fri_spinning; + std::thread _fri_loop_thread; - AsyncLBRClient &_client; + KUKA::FRI::UdpConnection _connection; + AsyncLBRClient &_client; KUKA::FRI::ClientApplication &_app; void _spin_fri() { - while (_success && _fri_loop_continue) { - _success = _app.step(); - if (_success) - _spinning = true; + while (_connected) { + + // Step the application + _connected = _app.step(); - if (_client.robotState().getSessionState() == - KUKA::FRI::ESessionState::IDLE) { - _success = false; - _spinning = false; - break; + // Update _fri_spinning variable + if (_connected) + _fri_spinning = true; + else + _fri_spinning = false; + + // Get session state + KUKA::FRI::ESessionState state = _client.robotState().getSessionState(); + + // Check session state + if (state == KUKA::FRI::ESessionState::IDLE) { + _connected = false; + _fri_spinning = false; } } } @@ -42,99 +51,34 @@ class AsyncClientApplication { AsyncClientApplication() : _client(*new AsyncLBRClient), _app(*new KUKA::FRI::ClientApplication(_connection, _client)), - _fri_loop_continue(false), _spinning(false) { - - std::vector Kp_position = {1., 1., 1., 1., 1., 1., 1.}; - std::vector Ki_position = - std::vector(KUKA::FRI::LBRState::NUMBER_OF_JOINTS, 0.0); - std::vector Kd_position = - std::vector(KUKA::FRI::LBRState::NUMBER_OF_JOINTS, 0.0); - _client.init_pid_position(Kp_position, Ki_position, Kd_position); - - std::vector Kp_wrench = {1., 1., 1., 1., 1., 1.}; - std::vector Ki_wrench = std::vector(NUM_CART_VEC, 0.0); - std::vector Kd_wrench = std::vector(NUM_CART_VEC, 0.0); - _client.init_pid_wrench(Kp_wrench, Ki_wrench, Kd_wrench); - - std::vector Kp_torque = {1., 1., 1., 1., 1., 1., 1.}; - std::vector Ki_torque = - std::vector(KUKA::FRI::LBRState::NUMBER_OF_JOINTS, 0.0); - std::vector Kd_torque = - std::vector(KUKA::FRI::LBRState::NUMBER_OF_JOINTS, 0.0); - _client.init_pid_torque(Kp_torque, Ki_torque, Kd_torque); - } + _connected(false), _fri_spinning(false) {} bool connect(const int port, char *const remoteHost = NULL) { // Connect to controller - _success = _app.connect(port, remoteHost); + _connected = _app.connect(port, remoteHost); // When successfull, start the fri loop - if (_success) - _fri_loop_continue = true; - _fri_loop_thread = std::thread(&AsyncClientApplication::_spin_fri, this); - - return _success; - } - - bool is_spinning() { return _spinning; } - - bool is_ok() { return _success; } + if (_connected) + _fri_loop_thread = std::thread(&AsyncClientApplication::_spin_fri, this); - std::vector get_ipo_position() const { - const double *p = _client.robotState().getIpoJointPosition(); - return std::vector(p, p + KUKA::FRI::LBRState::NUMBER_OF_JOINTS); + return _connected; } - std::vector get_measured_position() const { - const double *p = _client.robotState().getMeasuredJointPosition(); - return std::vector(p, p + KUKA::FRI::LBRState::NUMBER_OF_JOINTS); - } + bool is_ok() { return _connected && _fri_spinning; } - std::vector get_measured_torque() const { - const double *t = _client.robotState().getMeasuredTorque(); - return std::vector(t, t + KUKA::FRI::LBRState::NUMBER_OF_JOINTS); - } - - std::vector get_external_torque() const { - const double *t = _client.robotState().getExternalTorque(); - return std::vector(t, t + KUKA::FRI::LBRState::NUMBER_OF_JOINTS); - } - - std::vector get_proc_position() const { - return _client.get_proc_position(); - } - - std::vector get_proc_wrench() const { - return _client.get_proc_wrench(); - } - - std::vector get_proc_torque() const { - return _client.get_proc_torque(); - } - - std::vector get_set_position() const { - return _client.get_set_position(); - } - - std::vector get_set_wrench() const { - return _client.get_set_wrench(); - } - - std::vector get_set_torque() const { - return _client.get_set_torque(); - } - - void set_position(std::vector position) { - _client.set_position(position); + void disconnect() { + _app.disconnect(); + _connected = false; + _fri_spinning = false; } - void set_wrench(std::vector wrench) { _client.set_wrench(wrench); } + AsyncLBRClient client() { return _client; } - void set_torque(std::vector torque) { _client.set_torque(torque); } - - void disconnect() { - _app.disconnect(); - _fri_loop_continue = false; + void wait() { + while (!_fri_spinning) { + std::this_thread::sleep_for( + std::chrono::milliseconds(10)); // Sleep for 10 milliseconds + } } }; diff --git a/pyFRI/src/wrapper.cpp b/pyFRI/src/wrapper.cpp index ce06bc5..f091831 100644 --- a/pyFRI/src/wrapper.cpp +++ b/pyFRI/src/wrapper.cpp @@ -239,9 +239,30 @@ class PyClientApplication { } }; -// Python bindings +// Define py namespace namespace py = pybind11; +// Helper methods +void check_py_array(py::array_t arr, int size) { + if (arr.ndim() != 1 || PyArray_DIMS(arr.ptr())[0] != size) { + std::string errmsg = "Array must have shape ("; + errmsg += std::to_string(size); + errmsg += ",)."; + throw std::runtime_error(errmsg); + } +} + +double *cvt_py_array(py::array_t arr, int size) { + check_py_array(arr, size); + return static_cast(arr.request().ptr); +} + +void not_exposed(std::string name) { + throw std::runtime_error(name + " is not yet exposed."); +} + +// Python bindings + PYBIND11_MODULE(_pyFRI, m) { m.doc() = "Python bindings for the KUKA FRI Client SDK. THIS IS NOT A KUKA " "PRODUCT."; @@ -453,52 +474,32 @@ PYBIND11_MODULE(_pyFRI, m) { #elif FRI_VERSION_MAJOR == 2 .def("getMeasuredCartesianPose", [](const KUKA::FRI::LBRState &self) { - - // Declare variables - double data[KUKA::FRI::LBRState::NUMBER_OF_JOINTS]; - float dataf[KUKA::FRI::LBRState::NUMBER_OF_JOINTS]; - - // Retrieve state - memcpy(data, self.getMeasuredCartesianPose(), - KUKA::FRI::LBRState::NUMBER_OF_JOINTS * sizeof(double)); - - // Parse: double -> float - for (int i = 0; i < KUKA::FRI::LBRState::NUMBER_OF_JOINTS; i++) - dataf[i] = (float)data[i]; - - return py::array_t({KUKA::FRI::LBRState::NUMBER_OF_JOINTS}, - dataf); + not_exposed("getMeasuredCartesianPose"); }) .def("getMeasuredCartesianPoseAsMatrix", [](const KUKA::FRI::LBRState &self) { - // TODO - throw std::runtime_error("getMeasuredCartesianPoseAsMatrix is not yet exposed (use .getMeasuredCartesianPose instead)."); + not_exposed("getMeasuredCartesianPoseAsMatrix"); }) .def("getIpoCartesianPose", [](const KUKA::FRI::LBRState &self) { - // TODO - // Currently, FRI Cartesian Overlay is not supported by FRI-Client-SDK_Python. - // IPO Cartesian Pose not available when FRI Cartesian Overlay is not active. - throw std::runtime_error("getIpoCartesianPose is not yet exposed."); + not_exposed("getIpoCartesianPose"); }) .def("getIpoCartesianPoseAsMatrix", [](const KUKA::FRI::LBRState &self) { - // TODO - // Currently, FRI Cartesian Overlay is not supported by FRI-Client-SDK_Python. - // IPO Cartesian Pose not available when FRI Cartesian Overlay is not active. - throw std::runtime_error("getIpoCartesianPoseAsMatrix is not yet exposed."); + not_exposed("getIpoCartesianPoseAsMatrix"); }) .def("getMeasuredRedundancyValue", - &KUKA::FRI::LBRState::getMeasuredRedundancyValue) + [](KUKA::FRI::LBRState &self) { + not_exposed("getMeasuredRedundancyValue"); + }) .def("getIpoRedundancyValue", [](KUKA::FRI::LBRState &self) { - // TODO - // Currently, FRI Cartesian Overlay is not supported by FRI-Client-SDK_Python. - // IPO redundancy value not available when FRI Cartesian Overlay is not active. - throw std::runtime_error("getIpoRedundancyValue is not yet exposed."); + not_exposed("getIpoRedundancyValue"); }) .def("getRedundancyStrategy", - &KUKA::FRI::LBRState::getRedundancyStrategy) + [](KUKA::FRI::LBRState &self) { + not_exposed("getRedundancyStrategy"); + }) #endif ; // NOTE: this completes LBRState @@ -506,61 +507,23 @@ PYBIND11_MODULE(_pyFRI, m) { .def(py::init<>()) .def("setJointPosition", [](KUKA::FRI::LBRCommand &self, py::array_t values) { - if (values.ndim() != 1 || - PyArray_DIMS(values.ptr())[0] != - KUKA::FRI::LBRState::NUMBER_OF_JOINTS) { - throw std::runtime_error( - "Input array must have shape (" + - std::to_string(KUKA::FRI::LBRState::NUMBER_OF_JOINTS) + - ",)!"); - } - auto buf = values.request(); - const double *data = static_cast(buf.ptr); - self.setJointPosition(data); + self.setJointPosition(cvt_py_array(values, NDOF)); }) .def("setWrench", [](KUKA::FRI::LBRCommand &self, py::array_t values) { - if (values.ndim() != 1 || - PyArray_DIMS(values.ptr())[0] != - 6 // [F_x, F_y, F_z, tau_A, tau_B, tau_C] - ) { - throw std::runtime_error( - "Input array must have shape (" + - std::to_string(KUKA::FRI::LBRState::NUMBER_OF_JOINTS) + - ",)!"); - } - auto buf = values.request(); - const double *data = static_cast(buf.ptr); - self.setWrench(data); + self.setWrench(cvt_py_array(values, NCART)); }) .def("setTorque", [](KUKA::FRI::LBRCommand &self, py::array_t values) { - if (values.ndim() != 1 || - PyArray_DIMS(values.ptr())[0] != - KUKA::FRI::LBRState::NUMBER_OF_JOINTS) { - throw std::runtime_error( - "Input array must have shape (" + - std::to_string(KUKA::FRI::LBRState::NUMBER_OF_JOINTS) + - ",)!"); - } - auto buf = values.request(); - const double *data = static_cast(buf.ptr); - self.setTorque(data); + self.setTorque(cvt_py_array(values, NDOF)); }) .def("setCartesianPose", [](KUKA::FRI::LBRCommand &self, py::array_t values) { - // TODO - // Currently, FRI Cartesian Overlay is not supported by - // FRI-Client-SDK_Python. - throw std::runtime_error("setCartesianPose is not yet exposed."); + not_exposed("setCartesianPose"); }) .def("setCartesianPoseAsMatrix", [](KUKA::FRI::LBRCommand &self, py::array_t values) { - // TODO - // Currently, FRI Cartesian Overlay is not supported by - // FRI-Client-SDK_Python. - throw std::runtime_error( - "setCartesianPoseAsMatrix is not yet exposed."); + not_exposed("setCartesianPoseAsMatrix"); }) .def("setBooleanIOValue", &KUKA::FRI::LBRCommand::setBooleanIOValue) .def("setDigitalIOValue", &KUKA::FRI::LBRCommand::setDigitalIOValue) @@ -584,6 +547,42 @@ PYBIND11_MODULE(_pyFRI, m) { py::class_(m, "Rate").def(py::init()).def("sleep", &Rate::sleep); + py::class_(m, "AsyncClient") + .def(py::init<>()) + .def("set_pid_position_gains", + [](AsyncLBRClient &self, py::array_t Kp, + py::array_t Ki, py::array_t Kd) { + self.set_pid_position_gains(cvt_py_array(Kp, NDOF), + cvt_py_array(Ki, NDOF), + cvt_py_array(Kd, NDOF)); + }) + .def("set_pid_wrench_gains", + [](AsyncLBRClient &self, py::array_t Kp, + py::array_t Ki, py::array_t Kd) { + self.set_pid_wrench_gains(cvt_py_array(Kp, NCART), + cvt_py_array(Ki, NCART), + cvt_py_array(Kd, NCART)); + }) + .def("set_pid_torque_gains", + [](AsyncLBRClient &self, py::array_t Kp, + py::array_t Ki, py::array_t Kd) { + self.set_pid_torque_gains(cvt_py_array(Kp, NDOF), + cvt_py_array(Ki, NDOF), + cvt_py_array(Kd, NDOF)); + }) + .def("robotState", &AsyncLBRClient::robotState) + .def("set_position", + [](AsyncLBRClient &self, py::array_t position) { + self.set_position(cvt_py_array(position, NDOF)); + }) + .def("set_wrench", + [](AsyncLBRClient &self, py::array_t wrench) { + self.set_wrench(cvt_py_array(wrench, NCART)); + }) + .def("set_torque", [](AsyncLBRClient &self, py::array_t torque) { + self.set_torque(cvt_py_array(torque, NDOF)); + }); + py::class_(m, "AsyncClientApplication") .def(py::init([]() { auto app = @@ -593,171 +592,7 @@ PYBIND11_MODULE(_pyFRI, m) { return ptr; // Return the unique_ptr })) .def("connect", &AsyncClientApplication::connect) - .def("disconnect", &AsyncClientApplication::disconnect) + .def("wait", &AsyncClientApplication::wait) .def("is_ok", &AsyncClientApplication::is_ok) - .def("is_spinning", &AsyncClientApplication::is_spinning) - .def("get_ipo_position", - [](const AsyncClientApplication &self) { - float dataf[KUKA::FRI::LBRState::NUMBER_OF_JOINTS]; - std::vector data = self.get_ipo_position(); - - // Parse: double -> float - for (int i = 0; i < KUKA::FRI::LBRState::NUMBER_OF_JOINTS; i++) - dataf[i] = (float)data[i]; - - return py::array_t({KUKA::FRI::LBRState::NUMBER_OF_JOINTS}, - dataf); - }) - .def("get_measured_position", - [](const AsyncClientApplication &self) { - float dataf[KUKA::FRI::LBRState::NUMBER_OF_JOINTS]; - std::vector data = self.get_measured_position(); - - // Parse: double -> float - for (int i = 0; i < KUKA::FRI::LBRState::NUMBER_OF_JOINTS; i++) - dataf[i] = (float)data[i]; - - return py::array_t({KUKA::FRI::LBRState::NUMBER_OF_JOINTS}, - dataf); - }) - .def("get_measured_torque", - [](const AsyncClientApplication &self) { - float dataf[KUKA::FRI::LBRState::NUMBER_OF_JOINTS]; - std::vector data = self.get_measured_torque(); - - // Parse: double -> float - for (int i = 0; i < KUKA::FRI::LBRState::NUMBER_OF_JOINTS; i++) - dataf[i] = (float)data[i]; - - return py::array_t({KUKA::FRI::LBRState::NUMBER_OF_JOINTS}, - dataf); - }) - .def("get_external_torque", - [](const AsyncClientApplication &self) { - float dataf[KUKA::FRI::LBRState::NUMBER_OF_JOINTS]; - std::vector data = self.get_external_torque(); - - // Parse: double -> float - for (int i = 0; i < KUKA::FRI::LBRState::NUMBER_OF_JOINTS; i++) - dataf[i] = (float)data[i]; - - return py::array_t({KUKA::FRI::LBRState::NUMBER_OF_JOINTS}, - dataf); - }) - .def("get_proc_position", - [](const AsyncClientApplication &self) { - float dataf[KUKA::FRI::LBRState::NUMBER_OF_JOINTS]; - std::vector data = self.get_proc_position(); - - // Parse: double -> float - for (int i = 0; i < KUKA::FRI::LBRState::NUMBER_OF_JOINTS; i++) - dataf[i] = (float)data[i]; - - return py::array_t({KUKA::FRI::LBRState::NUMBER_OF_JOINTS}, - dataf); - }) - .def("get_proc_wrench", - [](const AsyncClientApplication &self) { - float dataf[NUM_CART_VEC]; - std::vector data = self.get_proc_wrench(); - - // Parse: double -> float - for (int i = 0; i < NUM_CART_VEC; i++) - dataf[i] = (float)data[i]; - - return py::array_t({NUM_CART_VEC}, dataf); - }) - .def("get_proc_torque", - [](const AsyncClientApplication &self) { - float dataf[KUKA::FRI::LBRState::NUMBER_OF_JOINTS]; - std::vector data = self.get_proc_torque(); - - // Parse: double -> float - for (int i = 0; i < KUKA::FRI::LBRState::NUMBER_OF_JOINTS; i++) - dataf[i] = (float)data[i]; - - return py::array_t({KUKA::FRI::LBRState::NUMBER_OF_JOINTS}, - dataf); - }) - .def("get_set_position", - [](const AsyncClientApplication &self) { - float dataf[KUKA::FRI::LBRState::NUMBER_OF_JOINTS]; - std::vector data = self.get_set_position(); - - // Parse: double -> float - for (int i = 0; i < KUKA::FRI::LBRState::NUMBER_OF_JOINTS; i++) - dataf[i] = (float)data[i]; - - return py::array_t({KUKA::FRI::LBRState::NUMBER_OF_JOINTS}, - dataf); - }) - .def("get_set_wrench", - [](const AsyncClientApplication &self) { - float dataf[NUM_CART_VEC]; - std::vector data = self.get_set_wrench(); - - // Parse: double -> float - for (int i = 0; i < NUM_CART_VEC; i++) - dataf[i] = (float)data[i]; - - return py::array_t({NUM_CART_VEC}, dataf); - }) - .def("get_set_torque", - [](const AsyncClientApplication &self) { - float dataf[KUKA::FRI::LBRState::NUMBER_OF_JOINTS]; - std::vector data = self.get_set_torque(); - - // Parse: double -> float - for (int i = 0; i < KUKA::FRI::LBRState::NUMBER_OF_JOINTS; i++) - dataf[i] = (float)data[i]; - - return py::array_t({KUKA::FRI::LBRState::NUMBER_OF_JOINTS}, - dataf); - }) - .def("set_joint_position", - [](AsyncClientApplication &self, py::array_t values) { - if (values.ndim() != 1 || - PyArray_DIMS(values.ptr())[0] != - KUKA::FRI::LBRState::NUMBER_OF_JOINTS) { - throw std::runtime_error( - "Input array must have shape (" + - std::to_string(KUKA::FRI::LBRState::NUMBER_OF_JOINTS) + - ",)!"); - } - auto buf = values.request(); - const double *data = static_cast(buf.ptr); - std::vector datav; - for (unsigned int i = 0; i < KUKA::FRI::LBRState::NUMBER_OF_JOINTS; - ++i) - datav.push_back(data[i]); - self.set_position(datav); - }) - .def("set_wrench", - [](AsyncClientApplication &self, py::array_t values) { - if (values.ndim() != 1 || - PyArray_DIMS(values.ptr())[0] != NUM_CART_VEC) { - throw std::runtime_error("Input array must have shape (6,)!"); - } - auto buf = values.request(); - const double *data = static_cast(buf.ptr); - std::vector datav; - for (unsigned int i = 0; i < NUM_CART_VEC; ++i) - datav.push_back(data[i]); - self.set_wrench(datav); - }) - .def("set_torque", [](AsyncClientApplication &self, - py::array_t values) { - if (values.ndim() != 1 || PyArray_DIMS(values.ptr())[0] != - KUKA::FRI::LBRState::NUMBER_OF_JOINTS) { - throw std::runtime_error( - "Input array must have shape (" + - std::to_string(KUKA::FRI::LBRState::NUMBER_OF_JOINTS) + ",)!"); - } - auto buf = values.request(); - const double *data = static_cast(buf.ptr); - std::vector datav; - for (unsigned int i = 0; i < KUKA::FRI::LBRState::NUMBER_OF_JOINTS; ++i) - datav.push_back(data[i]); - self.set_torque(datav); - }); + .def("disconnect", &AsyncClientApplication::disconnect); } From 20893ec87151a3d9be2d04676906e6ec70301c92 Mon Sep 17 00:00:00 2001 From: "Christopher E. Mower" Date: Tue, 8 Aug 2023 18:16:06 +0100 Subject: [PATCH 18/22] Revamp async example script --- examples/async_example.py | 86 ++++++++------------------------------- 1 file changed, 18 insertions(+), 68 deletions(-) diff --git a/examples/async_example.py b/examples/async_example.py index 7619036..c818408 100644 --- a/examples/async_example.py +++ b/examples/async_example.py @@ -36,27 +36,6 @@ def cvt_joint_mask(value): default=3, help="The joint to move.", ) - parser.add_argument( - "--freq-hz", - dest="freq_hz", - type=float, - default=0.25, - help="The frequency of the sine wave.", - ) - parser.add_argument( - "--ampl-rad", - dest="ampl_rad", - type=float, - default=0.04, - help="Applitude of the sine wave.", - ) - parser.add_argument( - "--filter-coeff", - dest="filter_coeff", - type=float, - default=0.99, - help="Exponential smoothing coeficient.", - ) return parser.parse_args() @@ -64,67 +43,38 @@ def cvt_joint_mask(value): def main(): print("Running FRI Version:", fri.FRI_VERSION) + # Get arguments and initialize client application args = get_arguments() - app = fri.AsyncClientApplication() + + # Set PID position gains + pos_Kp = np.array([1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0]) + pos_Ki = np.array([0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]) + pos_Kd = np.array([0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]) + app.set_pid_position_gains(pos_Kp, pos_Ki, pos_Kd) + + # Connect to controller if app.connect(args.port, args.hostname): print("Connected to KUKA Sunrise controller.") else: print("Connection to KUKA Sunrise controller failed.") return - # Wait for FRI loop to start spinning - # try: - rate_wait = fri.Rate(1) - counter = 1 - max_counter = 5 - spinning = app.is_spinning() - print("spinning=", spinning) - while not spinning: - print("Waiting for FRI loop to start, attempt", counter, "of", max_counter) - counter += 1 - if counter == max_counter + 1: - print("FRI loop did not start, quitting") - return - rate_wait.sleep() - spinning = app.is_spinning() - print("spinning=", spinning) - time.sleep(10.) - # except KeyboardInterrupt: - # pass - # finally: - # app.disconnect() - # print("Goodbye for now") - # return - + # Wait for FRI loop to start + app.wait() print("FRI Loop started") + # Setup for Python loop hz = 10 - time_step = 1.0 / float(hz) - - q0 = app.get_proc_position() - offset = 0.0 - phi = 0.0 - step_width = 2 * math.pi * args.freq_hz * time_step + dt = 1.0 / float(hz) + rate = fri.Rate(hz) + q = app.robotState().getIpoJointPosition() try: - rate = fri.Rate(hz) - t = 0. + t = 0.0 while app.is_ok(): - new_offset = args.ampl_rad * math.sin(phi) - offset = (offset * args.filter_coeff) + ( - new_offset * (1.0 - args.filter_coeff) - ) - phi += step_width - if phi >= (2 * math.pi): - phi -= 2 * math.pi - # q[args.joint_mask] += offset - q = q0.copy() - q[args.joint_mask] += math.radians(20)*math.sin(t*0.) - # print(q) - app.set_joint_position(q.astype(np.float32)) - qm = app.get_measured_position() - print(np.linalg.norm(qm - q)) + q[args.joint_mask] += math.radians(20) * math.sin(t * 0.01) + app.set_position(q.astype(np.float32)) rate.sleep() t += time_step except KeyboardInterrupt: From c1448ab1dfe79b16a971bca9c394829c5af80b78 Mon Sep 17 00:00:00 2001 From: "Christopher E. Mower" Date: Tue, 8 Aug 2023 20:15:50 +0100 Subject: [PATCH 19/22] Add execution types section in readme --- README.md | 41 +++++++++++++++++++++++++++++++++++++++++ doc/sync-vs-async.png | Bin 0 -> 113388 bytes 2 files changed, 41 insertions(+) create mode 100644 doc/sync-vs-async.png diff --git a/README.md b/README.md index 51e68f9..766f3d7 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,8 @@ app = fri.ClientApplication(client) Since UDP is the only supported connection type and the connection object is not actually required by the user after declaring the variable, the `UdpConnection` object is created internally to the `fri.ClientApplication` class object. +The `pyFRI` library also supports asynchronous execution, see *Execution types* section below. + See the [examples](examples/). # Important notice @@ -38,6 +40,45 @@ See the [examples](examples/). [@cmower](https://github.com/cmower) is not affiliated with KUKA. +# Execution types + +

+ +

+ +Two execution types are supported: (i) synchronous, and (ii) asynchronous. +These are both shown in the figure above. + +## Synchronous + +This constitutes the operational approach embraced by FRI. +Conceptually, you can envision this approach as executing the subsequent actions: + +1. The KUKA controller sends a message to the LBR client over a UDP connection. +2. A response is computed (using some method defined in the client application). +3. The commanded response is encoded and sent back to the controller. +4. The physical robot moves according the command and controller type selected in the Java application. + +These steps are repeated at a sampling rate defined in the Java application, e.g. 200Hz. + +The pyFRI library abstracts the functionalities of the `ClientApplication` and `LBRClient` classes, enabling users to craft application scripts using classes/functions that mirror the examples provided in the FRI C++ documentation. +An added benefit is the availability of KUKA's FRI documentation for C++, which can serve as a guide for pyFRI users. + +The drawback for this approach is the execution loop in the Python application must fit within the sampling frequency set by the Java application. +As such, higher sampling frequencies (i.e. 500-1000Hz) can be difficult to achieve using pyFRI. + +## Asynchronous + +The pyFRI library incorporates an asynchronous execution approach, allowing users to execute FRI communication at various permissible sampling frequencies (i.e., 100-1000Hz), along with a distinct sampling frequency for the loop on the Python application's end. +The FRI communication on the C++ side is executed on another thread and shared memory between the C++ client and Python application is used to define the target joint states. + +In order to ensure smooth robot motion, a PID controller is implemented where the user specifies the set target. +The process variable is executed on the robot using an open-loop PID controller. + +The advantage of employing this execution approach lies in the flexibility to configure the controller to operate at the user's preferred frequency, while the Python loop can operate at a lower frequency. +This proves particularly useful during when implementing Model Predictive Control. +However, a downside of this method is the necessity for precise tuning of the PID controller. + # Support The following versions of FRI are currently supported: diff --git a/doc/sync-vs-async.png b/doc/sync-vs-async.png new file mode 100644 index 0000000000000000000000000000000000000000..1ee1341e6e0c29cd1f30323a7f4027913244fc00 GIT binary patch literal 113388 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 literal 0 HcmV?d00001 From b90c399d2f27d92e9392a514c027c16cd12eeffb Mon Sep 17 00:00:00 2001 From: "Christopher E. Mower" Date: Tue, 8 Aug 2023 20:18:07 +0100 Subject: [PATCH 20/22] Add pointer to examples --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 766f3d7..6fa9c7f 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,8 @@ An added benefit is the availability of KUKA's FRI documentation for C++, which The drawback for this approach is the execution loop in the Python application must fit within the sampling frequency set by the Java application. As such, higher sampling frequencies (i.e. 500-1000Hz) can be difficult to achieve using pyFRI. +The majority of the examples use the synchronous execution style. + ## Asynchronous The pyFRI library incorporates an asynchronous execution approach, allowing users to execute FRI communication at various permissible sampling frequencies (i.e., 100-1000Hz), along with a distinct sampling frequency for the loop on the Python application's end. @@ -79,6 +81,8 @@ The advantage of employing this execution approach lies in the flexibility to co This proves particularly useful during when implementing Model Predictive Control. However, a downside of this method is the necessity for precise tuning of the PID controller. +See the [examples/async_example.py](examples/async_example.py) example script. + # Support The following versions of FRI are currently supported: From 3e9f5a758354eee943e3b386e4ddb71973a6512a Mon Sep 17 00:00:00 2001 From: "Christopher E. Mower" Date: Thu, 10 Aug 2023 16:53:13 +0100 Subject: [PATCH 21/22] Expose async client and update example --- examples/async_example.py | 6 +++--- pyFRI/src/wrapper.cpp | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/examples/async_example.py b/examples/async_example.py index c818408..3616ee6 100644 --- a/examples/async_example.py +++ b/examples/async_example.py @@ -51,7 +51,7 @@ def main(): pos_Kp = np.array([1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0]) pos_Ki = np.array([0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]) pos_Kd = np.array([0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]) - app.set_pid_position_gains(pos_Kp, pos_Ki, pos_Kd) + app.client().set_pid_position_gains(pos_Kp, pos_Ki, pos_Kd) # Connect to controller if app.connect(args.port, args.hostname): @@ -68,13 +68,13 @@ def main(): hz = 10 dt = 1.0 / float(hz) rate = fri.Rate(hz) - q = app.robotState().getIpoJointPosition() + q = app.client().robotState().getIpoJointPosition() try: t = 0.0 while app.is_ok(): q[args.joint_mask] += math.radians(20) * math.sin(t * 0.01) - app.set_position(q.astype(np.float32)) + app.client().set_position(q.astype(np.float32)) rate.sleep() t += time_step except KeyboardInterrupt: diff --git a/pyFRI/src/wrapper.cpp b/pyFRI/src/wrapper.cpp index f091831..db1e1e9 100644 --- a/pyFRI/src/wrapper.cpp +++ b/pyFRI/src/wrapper.cpp @@ -594,5 +594,6 @@ PYBIND11_MODULE(_pyFRI, m) { .def("connect", &AsyncClientApplication::connect) .def("wait", &AsyncClientApplication::wait) .def("is_ok", &AsyncClientApplication::is_ok) + .def("client", &AsyncClientApplication::client) .def("disconnect", &AsyncClientApplication::disconnect); } From 9d52e2b57926e09634f1416494b95de1e8dfcd35 Mon Sep 17 00:00:00 2001 From: "Christopher E. Mower" Date: Thu, 10 Aug 2023 16:53:26 +0100 Subject: [PATCH 22/22] Enable user to quit from application --- pyFRI/src/async_client_application.cpp | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/pyFRI/src/async_client_application.cpp b/pyFRI/src/async_client_application.cpp index 466c326..b488f7e 100644 --- a/pyFRI/src/async_client_application.cpp +++ b/pyFRI/src/async_client_application.cpp @@ -1,4 +1,5 @@ // Standard library +#include #include // KUKA FRI-Client-SDK_Cpp (using version hosted at: @@ -19,6 +20,8 @@ class AsyncClientApplication { std::thread _fri_loop_thread; + struct sigaction _sig_int_handler; + KUKA::FRI::UdpConnection _connection; AsyncLBRClient &_client; KUKA::FRI::ClientApplication &_app; @@ -51,7 +54,17 @@ class AsyncClientApplication { AsyncClientApplication() : _client(*new AsyncLBRClient), _app(*new KUKA::FRI::ClientApplication(_connection, _client)), - _connected(false), _fri_spinning(false) {} + _connected(false), _fri_spinning(false) { + _sig_int_handler.sa_handler = + &AsyncClientApplication::wait_exception_handler; + sigemptyset(&_sig_int_handler.sa_mask); + _sig_int_handler.sa_flags = 0; + sigaction(SIGINT, &_sig_int_handler, NULL); + } + + static void wait_exception_handler(int s) { + throw std::runtime_error("quitting async client application"); + } bool connect(const int port, char *const remoteHost = NULL) {