diff --git a/CLAUDE.md b/CLAUDE.md index 11dcc13..3c901ab 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -24,7 +24,11 @@ make To build a single plugin, run qmake/make inside its subdirectory (e.g., `PluginExample/PluginExample.pro`). -There are no automated tests — plugins are tested manually by loading them in RoboDK via **Tools → Plug-Ins**. +There are no automated tests — plugins are tested manually by loading them in RoboDK via **Tools → Plug-Ins**, or faster, by launching RoboDK with a command-line flag: +```bash +RoboDK.exe -PLUGINSLOAD # start with all available plugins loaded +RoboDK.exe -PLUGINLOAD=C:/RoboDK/bin/plugins/pluginexample.dll # load one plugin on the fly +``` ## Qt Version Requirements @@ -79,7 +83,7 @@ PluginXxx/ ### Add-in Apps (`PluginAppLoader/Apps/`) -Python-based apps loaded via the `PluginAppLoader` C++ plugin. Each app in its own subdirectory follows the same `IAppRoboDK` interface conventions but runs as Python scripts rather than compiled DLLs. +Python-based apps loaded via the `PluginAppLoader` C++ plugin (built in separately, always enabled in RoboDK but disabled by default). Each app is a subfolder; every `.py` script inside becomes a menu item/toolbar button, and an `AppConfig.ini` (auto-generated on first load) controls menu placement, icons, and shortcuts. Package an app folder into a distributable `.rdkp` with `PluginAppLoader/Apps/PackageCreate.py` (or `PackageCreateOne.py` for a single app). ### Robot Extensions (`robotextensions/samplekinematics/`) diff --git a/PluginExample/README.md b/PluginExample/README.md new file mode 100644 index 0000000..3eaa1e6 --- /dev/null +++ b/PluginExample/README.md @@ -0,0 +1,211 @@ +# Plugin Example + +Plugin Example is the reference RoboDK Plug-in used as a starting point for developing new plug-ins. It demonstrates the +Plug-in Interface: registering menu/toolbar actions, adding a docked window, listening to RoboDK events, and +calling the RoboDK API (`IRoboDK`/`IItem`) directly from C++ for maximum performance. + +![Speed benchmark docked window showing kinematics and collision timing results](benchmark-info.png) + + +## Features + +- **Plugin Speed Information** (`Ctrl+I`): runs a benchmark of the RoboDK API on the selected robot (Forward + Kinematics, Inverse Kinematics, and collision checking) and reports the results in a docked window. +- **Program collision check**: optionally checks every step of a program for collisions and reports how many + points are in collision vs. collision-free, along with timing statistics. +- Includes a **System / CPU / RAM** summary of the computer running the benchmark. +- **Robot Pilot Form**: a docked window to jog the robot by incremental steps (joints or Cartesian, relative to + the tool or the reference frame). +- Right-click integration: the benchmark and robot pilot actions are added to the context menu of objects and + robots in the station tree. + +## Usage + +1. Select **Tools → Plug-Ins** in RoboDK and double click on **PluginExample** if it is not already loaded. +2. Use the **Plugin Example** menu or toolbar: + - **Plugin Speed Information** (or `Ctrl+I`): pick a robot to benchmark. You can then optionally pick a + program to run a collision check against; if a program named `Main` exists, it is used automatically. + - **Robot Pilot Form**: opens the jog panel described above. + - **RoboDK Plugins - Help**: opens the RoboDK Plug-in documentation in your browser. +3. Results are displayed as a table in a docked window and are also printed as an aligned plain-text table in + the console/debug output, which is useful when there is no GUI available (see below). + + +## Files + +| File | Description | +|------|-------------| +| `pluginexample.h` / `.cpp` | Plugin entry point: `IAppRoboDK` implementation, menu/toolbar setup, benchmark logic | +| `formrobotpilot.h` / `.cpp` / `.ui` | Robot Pilot docked widget | +| `run_api_benchmark.py` | Standalone Python script that runs the same kind of benchmarks using the RoboDK API for Python | +| `run_plugin_benchmark.py` | Python helper that downloads the sample station, starts a headless RoboDK instance, loads this plugin, and runs the benchmark automatically via `robolink` | +| `manifest.xml` | Add-in package metadata | + +## Getting benchmark results from the command line + +The benchmark can be triggered without any user interaction using RoboDK's `-PluginCommand` argument, which +calls `PluginCommand("BenchmarkInfo", progname)`. This is useful to collect performance stats headlessly, for +example as part of an automated test or CI (Continuous Integration) workflow: + +```bash +./RoboDK -NEWINSTANCE -NOUI -SKIPINI -PLUGINLOAD=PluginExample "C:/RoboDK/Library/Welding-with-Comau-Smart5-NJ-130-2-6.rdk" -PluginCommand=BenchmarkInfo=MainProg +``` + +- `-NEWINSTANCE`: starts a new RoboDK instance instead of reusing one that is already running. +- `-NOUI`: runs RoboDK without showing its main window. +- `-SKIPINI`: skips loading the user's saved settings/station list. +- `-PLUGINLOAD=PluginExample`: loads this plugin on startup. +- The quoted path is the RoboDK station (`.rdk`) to open. +- `-PluginCommand=BenchmarkInfo=MainProg`: runs the benchmark against the program named `MainProg` and prints + the results as text to the console. Since `-NOUI` means the docked report is never shown, this text output + is the only way to read the stats in this scenario. To save the output to a file when triggering the action + manually, launch RoboDK with the `-DEBUG` flag (`C:/RoboDK/RoboDK-Debug.bat` on Windows). + +Alternatively, `run_plugin_benchmark.py` automates the same steps via the Python `robolink` API: it downloads +the sample station, starts a headless RoboDK instance, loads the plugin, and streams the benchmark output to +stdout. + +## Performance results (RoboDK v6) + +The following results were obtained using the +[Spot welding station with Comau](https://robodk.com/example/Welding-with-Comau-Smart5-NJ-130-2-6) and +RoboDK v6.0.6. RoboDK v6 includes important performance improvements for collision checking compared to +previous versions. + +> **Note:** These results illustrate RoboDK performance on the systems tested and should not be interpreted +> as a controlled comparison of processors or operating systems. Performance may vary depending on the RoboDK +> build, station complexity, collision settings, system configuration, power settings, and background processes. + +### Summary + +| System | Forward Kinematics | Inverse Kinematics | Collision rate (5 samples) | Collision rate (full program) | +|--------|-------------------:|-------------------:|---------------------------:|------------------------------:| +| i9-14900KF — Windows (Desktop) | 0.61 µs | 2.00 µs | 744 samples/sec | 219 samples/sec | +| i7-1165G7 — Windows (Laptop) | 1.57 µs | 11.36 µs | 524 samples/sec | 83 samples/sec | +| Apple M5 Pro — native build | 0.30 µs | 1.39 µs | 4357 samples/sec | 1204 samples/sec | +| Apple M1 — native build | 0.99 µs | 3.59 µs | 4196 samples/sec | 725 samples/sec | +| Apple M1 — Intel build (Rosetta 2) | 2.09 µs | 6.16 µs | 3219 samples/sec | 707 samples/sec | + +### Results on Windows + +#### Intel Core i9-14900KF @ 3.19 GHz (Desktop PC) + +``` +Metric Value +-------------------------------------------------- +Robot Comau Smart5 NJ 130-2.6 +System Windows 11 Version 2009 +CPU Intel(R) Core(TM) i9-14900KF, 32 cores @ 3.19 GHz +RAM 31.8 GB +Forward Kinematics 0.61 microseconds +Inverse Kinematics 2.00 microseconds +Inverse Kinematics (all solutions) 2.23 microseconds +Collision check (5 samples) 1.34 ms/sample +Collision check rate 743.55 samples/sec +Points with collisions 0 +Points without collisions 5 + +-- Program Collision Check: main -- +Collision check (3589 steps) 4.57 ms/step +Collision check rate 218.99 samples/sec +Points with collisions 66 +Points without collisions 3523 +``` + +#### Intel Core i7-1165G7 @ 2.8 GHz (Laptop PC) + +``` +Metric Value +-------------------------------------------------- +Robot Comau Smart5 NJ 130-2.6 +System Windows 11 Version 2009 +CPU 11th Gen Intel(R) Core(TM) i7-1165G7 @ 2.80GHz, 8 cores @ 2.80 GHz +RAM 15.6 GB +Forward Kinematics 1.57 microseconds +Inverse Kinematics 11.36 microseconds +Inverse Kinematics (all solutions) 11.88 microseconds +Collision check (5 samples) 1.91 ms/sample +Collision check rate 523.58 samples/sec +Points with collisions 0 +Points without collisions 5 + +-- Program Collision Check: main -- +Collision check (3589 steps) 12.07 ms/step +Collision check rate 82.88 samples/sec +Points with collisions 66 +Points without collisions 3523 +``` + +### Results on macOS + +#### Apple M5 Pro — Apple Silicon build + +``` +Metric Value +-------------------------------------------------- +Robot Comau Smart5 NJ 130-2.6 +System macOS Tahoe (26.5.2) +CPU Apple M5 Pro, 15 cores +RAM 24.0 GB +Forward Kinematics 0.30 microseconds +Inverse Kinematics 1.39 microseconds +Inverse Kinematics (all solutions) 1.36 microseconds +Collision check (5 samples) 0.23 ms/sample +Collision check rate 4357.14 samples/sec +Points with collisions 0 +Points without collisions 5 + +-- Program Collision Check: main -- +Collision check (3589 steps) 0.83 ms/step +Collision check rate 1203.55 samples/sec +Points with collisions 66 +Points without collisions 3523 +``` + +#### Apple M1 — Apple Silicon build + +``` +Metric Value +-------------------------------------------------- +Robot Comau Smart5 NJ 130-2.6 +System macOS Tahoe (26.5) +CPU Apple M1, 8 cores +RAM 16.0 GB +Forward Kinematics 0.99 microseconds +Inverse Kinematics 3.59 microseconds +Inverse Kinematics (all solutions) 2.65 microseconds +Collision check (5 samples) 0.24 ms/sample +Collision check rate 4196.39 samples/sec +Points with collisions 0 +Points without collisions 5 + +-- Program Collision Check: main -- +Collision check (3589 steps) 1.38 ms/step +Collision check rate 724.92 samples/sec +Points with collisions 66 +Points without collisions 3523 +``` + +#### Apple M1 — Intel build (Rosetta 2) + +``` +Metric Value +-------------------------------------------------- +Robot Comau Smart5 NJ 130-2.6 +System macOS Tahoe (26.5) +CPU Apple M1, 8 cores @ 2.40 GHz +RAM 16.0 GB +Forward Kinematics 2.09 microseconds +Inverse Kinematics 6.16 microseconds +Inverse Kinematics (all solutions) 5.24 microseconds +Collision check (5 samples) 0.31 ms/sample +Collision check rate 3219.49 samples/sec +Points with collisions 0 +Points without collisions 5 + +-- Program Collision Check: main -- +Collision check (3589 steps) 1.41 ms/step +Collision check rate 707.07 samples/sec +Points with collisions 66 +Points without collisions 3523 +``` diff --git a/PluginExample/benchmark-info.png b/PluginExample/benchmark-info.png new file mode 100644 index 0000000..c67db6e Binary files /dev/null and b/PluginExample/benchmark-info.png differ diff --git a/PluginExample/formrobotpilot.cpp b/PluginExample/formrobotpilot.cpp index 22f2334..0c673b1 100644 --- a/PluginExample/formrobotpilot.cpp +++ b/PluginExample/formrobotpilot.cpp @@ -2,11 +2,9 @@ #include "ui_formrobotpilot.h" FormRobotPilot::FormRobotPilot(RoboDK *rdk, QWidget *parent) : QWidget(parent), - ui(new Ui::FormRobotPilot) -{ - // keep the pointer to RoboDK - RDK = rdk; - Robot = nullptr; + ui(new Ui::FormRobotPilot), + RDK(rdk), + Robot(nullptr) { // Create the window ui->setupUi(this); @@ -24,17 +22,22 @@ FormRobotPilot::FormRobotPilot(RoboDK *rdk, QWidget *parent) : QWidget(parent), SelectRobot(); } -FormRobotPilot::~FormRobotPilot(){ +FormRobotPilot::~FormRobotPilot() { delete ui; } -void FormRobotPilot::on_btnSelectRobot_clicked(){ - SelectRobot(); +void FormRobotPilot::on_btnSelectRobot_clicked() { + SelectRobot(true); } -bool FormRobotPilot::SelectRobot(){ +bool FormRobotPilot::SelectRobot(bool force_selection) { + if (!force_selection && RDK->Valid(Robot)) { + // a valid robot is already selected: avoid prompting on every jog move + return true; + } + QList all_robots = RDK->getItemList(IItem::ITEM_TYPE_ROBOT); - if (all_robots.length() == 0){ + if (all_robots.length() == 0) { ui->lblRobot->setText("Load a robot"); RDK->ShowMessage("Select File-Open to load a robot or a RoboDK station", false); //QString file = QFileDialog::getOpenFilename() @@ -52,9 +55,7 @@ bool FormRobotPilot::SelectRobot(){ return robot_is_selected; } - - -void FormRobotPilot::setup_btn_joints(){ +void FormRobotPilot::setup_btn_joints() { ui->btnTXn->setText("J1-"); ui->btnTXp->setText("J1+"); ui->btnTYn->setText("J2-"); @@ -68,7 +69,8 @@ void FormRobotPilot::setup_btn_joints(){ ui->btnRZn->setText("J6-"); ui->btnRZp->setText("J6+"); } -void FormRobotPilot::setup_btn_cartesian(){ + +void FormRobotPilot::setup_btn_cartesian() { ui->btnTXn->setText("Tx-"); ui->btnTXp->setText("Tx+"); ui->btnTYn->setText("Ty-"); @@ -82,58 +84,58 @@ void FormRobotPilot::setup_btn_cartesian(){ ui->btnRZn->setText("Rz-"); ui->btnRZp->setText("Rz+"); } -void FormRobotPilot::on_radCartesianReference_clicked(){ + +void FormRobotPilot::on_radCartesianReference_clicked() { setup_btn_cartesian(); } -void FormRobotPilot::on_radCartesianTool_clicked() -{ +void FormRobotPilot::on_radCartesianTool_clicked() { setup_btn_cartesian(); } -void FormRobotPilot::on_radJoints_clicked() -{ +void FormRobotPilot::on_radJoints_clicked() { setup_btn_joints(); } -void FormRobotPilot::on_btnTXn_clicked(){ IncrementalMove(0, -1); } -void FormRobotPilot::on_btnTYn_clicked(){ IncrementalMove(1, -1); } -void FormRobotPilot::on_btnTZn_clicked(){ IncrementalMove(2, -1); } -void FormRobotPilot::on_btnRXn_clicked(){ IncrementalMove(3, -1); } -void FormRobotPilot::on_btnRYn_clicked(){ IncrementalMove(4, -1); } -void FormRobotPilot::on_btnRZn_clicked(){ IncrementalMove(5, -1); } - -void FormRobotPilot::on_btnTXp_clicked(){ IncrementalMove(0, +1); } -void FormRobotPilot::on_btnTYp_clicked(){ IncrementalMove(1, +1); } -void FormRobotPilot::on_btnTZp_clicked(){ IncrementalMove(2, +1); } -void FormRobotPilot::on_btnRXp_clicked(){ IncrementalMove(3, +1); } -void FormRobotPilot::on_btnRYp_clicked(){ IncrementalMove(4, +1); } -void FormRobotPilot::on_btnRZp_clicked(){ IncrementalMove(5, +1); } - - -void FormRobotPilot::IncrementalMove(int id, double sense){ - if (!SelectRobot()) { return; } +void FormRobotPilot::on_btnTXn_clicked() { IncrementalMove(0, -1); } +void FormRobotPilot::on_btnTYn_clicked() { IncrementalMove(1, -1); } +void FormRobotPilot::on_btnTZn_clicked() { IncrementalMove(2, -1); } +void FormRobotPilot::on_btnRXn_clicked() { IncrementalMove(3, -1); } +void FormRobotPilot::on_btnRYn_clicked() { IncrementalMove(4, -1); } +void FormRobotPilot::on_btnRZn_clicked() { IncrementalMove(5, -1); } + +void FormRobotPilot::on_btnTXp_clicked() { IncrementalMove(0, +1); } +void FormRobotPilot::on_btnTYp_clicked() { IncrementalMove(1, +1); } +void FormRobotPilot::on_btnTZp_clicked() { IncrementalMove(2, +1); } +void FormRobotPilot::on_btnRXp_clicked() { IncrementalMove(3, +1); } +void FormRobotPilot::on_btnRYp_clicked() { IncrementalMove(4, +1); } +void FormRobotPilot::on_btnRZp_clicked() { IncrementalMove(5, +1); } + +void FormRobotPilot::IncrementalMove(int id, double sense) { + if (!SelectRobot()) { + return; + } // Calculate the relative movement double step = sense * ui->spnStep->value(); // check if it is a joint movement or a Cartesian movement bool is_joint_move = ui->radJoints->isChecked(); - if (is_joint_move){ + if (is_joint_move) { tJoints joints = Robot->Joints(); - if (id >= joints.Length()){ + if (id >= joints.Length()) { qDebug() << "Internal problem: Invalid joint ID"; return; } joints.Data()[id] = joints.Data()[id] + step; bool can_move = Robot->MoveJ(joints); - if (!can_move){ + if (!can_move) { RDK->ShowMessage(tr("The robot can't move to this location"), false); } } else { - // check the index so that is is within 0-5 - if (id < 0 || id >= 6){ - qDebug()<< "Internal problem: Invalid id provided for an incremental move"; + // check the index so that it is within 0-5 + if (id < 0 || id >= 6) { + qDebug() << "Internal problem: Invalid id provided for an incremental move"; return; } @@ -150,7 +152,7 @@ void FormRobotPilot::IncrementalMove(int id, double sense){ Mat pose_robot_new; bool is_tcp_relative_move = ui->radCartesianTool->isChecked(); - if (is_tcp_relative_move){ + if (is_tcp_relative_move) { // apply relative to the TCP: // if the movement is relative to the TCP we must POST MULTIPLY the movement pose_robot_new = pose_robot * pose_increment; @@ -167,7 +169,7 @@ void FormRobotPilot::IncrementalMove(int id, double sense){ } bool canmove = Robot->MoveJ(pose_robot_new); - if (!canmove){ + if (!canmove) { RDK->ShowMessage(tr("The robot can't move to this location"), false); } /*if (!Robot->setPose(pose_robot_new)){ @@ -179,10 +181,7 @@ void FormRobotPilot::IncrementalMove(int id, double sense){ RDK->Render(); } - - - -void FormRobotPilot::on_chkRunOnRobot_clicked(bool checked){ +void FormRobotPilot::on_chkRunOnRobot_clicked(bool checked) { if (checked) { if (!SelectRobot()) { RDK->ShowMessage(tr("Select a robot first"), false); diff --git a/PluginExample/formrobotpilot.h b/PluginExample/formrobotpilot.h index d06ee9c..d98dd6b 100644 --- a/PluginExample/formrobotpilot.h +++ b/PluginExample/formrobotpilot.h @@ -22,8 +22,9 @@ class FormRobotPilot : public QWidget ~FormRobotPilot(); /// \brief Select a robot in the Robot variable + /// \param force_selection Set to true to always prompt the selection dialog, even if a valid robot is already selected /// \return True if a robot was properly retrieved - bool SelectRobot(); + bool SelectRobot(bool force_selection = false); /// \brief IncrementalMove /// \param id Joint id or cartesian move id [x,y,z,r,p,w] diff --git a/PluginExample/pluginexample.cpp b/PluginExample/pluginexample.cpp index 4a7a8b2..4a34b37 100644 --- a/PluginExample/pluginexample.cpp +++ b/PluginExample/pluginexample.cpp @@ -9,22 +9,40 @@ #include #include #include +#include #include #include #include #include #include #include +#include +#include +#include +#include +#include + +// Platform-specific headers used only to read the CPU model/frequency and total RAM +// (Qt does not expose this information through a cross-platform API) +#if defined(Q_OS_WIN) +#include +#include +#elif defined(Q_OS_MACOS) +#include +#elif defined(Q_OS_LINUX) +#include +#include +#endif //------------------------------- RoboDK Plug-in commands ------------------------------ -QString PluginExample::PluginName(){ +QString PluginExample::PluginName() { return "Example Plugin"; } -QString PluginExample::PluginLoad(QMainWindow *mw, QMenuBar *menubar, QStatusBar *statusbar, RoboDK *rdk, const QString &settings){ +QString PluginExample::PluginLoad(QMainWindow *mw, QMenuBar *menubar, QStatusBar *statusbar, RoboDK *rdk, const QString &settings) { RDK = rdk; MainWindow = mw; StatusBar = statusbar; @@ -39,18 +57,20 @@ QString PluginExample::PluginLoad(QMainWindow *mw, QMenuBar *menubar, QStatusBar Q_INIT_RESOURCE(resources1); // Here you can add all the "Actions": these actions are callbacks from buttons selected from the menu or the toolbar - action_information = new QAction(QIcon(":/resources/information.png"), tr("Plugin Speed Information")); + action_benchmarkInfo = new QAction(QIcon(":/resources/information.png"), tr("Plugin Speed Information")); + action_benchmarkInfo->setShortcut(QKeySequence("Ctrl+I")); + action_benchmarkInfo->setObjectName("actionBenchmarkInfo"); action_robotpilot = new QAction(QIcon(":/resources/code.png"), tr("Robot Pilot Form")); action_help = new QAction(QIcon(":/resources/help.png"), tr("RoboDK Plugins - Help")); // Make sure to connect the action to your callback (slot) - connect(action_information, SIGNAL(triggered()), this, SLOT(callback_information()), Qt::QueuedConnection); + connect(action_benchmarkInfo, SIGNAL(triggered()), this, SLOT(callback_benchmarkInfo()), Qt::QueuedConnection); connect(action_robotpilot, SIGNAL(triggered()), this, SLOT(callback_robotpilot()), Qt::QueuedConnection); connect(action_help, SIGNAL(triggered()), this, SLOT(callback_help()), Qt::QueuedConnection); // Here you can add one or more actions in the menu menu1 = menubar->addMenu("Plugin Example Menu"); qDebug() << "Setting up the menu bar"; - menu1->addAction(action_information); + menu1->addAction(action_benchmarkInfo); menu1->addAction(action_robotpilot); menu1->addAction(action_help); @@ -58,12 +78,12 @@ QString PluginExample::PluginLoad(QMainWindow *mw, QMenuBar *menubar, QStatusBar dock_robotpilot = nullptr; form_robotpilot = nullptr; - // return string is reserverd for future compatibility + // return string is reserved for future compatibility return ""; -}; +} -void PluginExample::PluginUnload(){ +void PluginExample::PluginUnload() { // Cleanup the plugin qDebug() << "Unloading plugin " << PluginName(); @@ -74,7 +94,15 @@ void PluginExample::PluginUnload(){ toolbar1->deleteLater(); toolbar1 = nullptr; - if (dock_robotpilot != nullptr){ + // remove the actions (not owned by the menu/toolbar, so they are not deleted automatically) + action_benchmarkInfo->deleteLater(); + action_benchmarkInfo = nullptr; + action_robotpilot->deleteLater(); + action_robotpilot = nullptr; + action_help->deleteLater(); + action_help = nullptr; + + if (dock_robotpilot != nullptr) { dock_robotpilot->close(); dock_robotpilot = nullptr; form_robotpilot = nullptr; @@ -84,7 +112,7 @@ void PluginExample::PluginUnload(){ Q_CLEANUP_RESOURCE(resources1); } -void PluginExample::PluginLoadToolbar(QMainWindow *mw, int icon_size){ +void PluginExample::PluginLoadToolbar(QMainWindow *mw, int icon_size) { // Create a new toolbar: toolbar1 = mw->addToolBar("Plugin Example Toolbar"); toolbar1->setIconSize(QSize(icon_size, icon_size)); @@ -93,21 +121,21 @@ void PluginExample::PluginLoadToolbar(QMainWindow *mw, int icon_size){ toolbar1->setObjectName(PluginName() + "-Toolbar1"); // Add a new button to the toolbar - toolbar1->addAction(action_information); + toolbar1->addAction(action_benchmarkInfo); toolbar1->addAction(action_robotpilot); toolbar1->addAction(action_help); } -bool PluginExample::PluginItemClick(Item item, QMenu *menu, TypeClick click_type){ +bool PluginExample::PluginItemClick(Item item, QMenu *menu, TypeClick click_type) { qDebug() << "Selected item: " << item->Name() << " of type " << item->Type() << " click type: " << click_type; - if (item->Type() == IItem::ITEM_TYPE_OBJECT){ + if (item->Type() == IItem::ITEM_TYPE_OBJECT) { //menu->actions().insert(0, action_btn1); // add action at the beginning - menu->addAction(action_information); // add action at the end + menu->addAction(action_benchmarkInfo); // add action at the end qDebug() << "Done"; return false; - } else if (item->Type() == IItem::ITEM_TYPE_ROBOT){ + } else if (item->Type() == IItem::ITEM_TYPE_ROBOT) { //menu->actions().insert(0, action_robotpilot); // add action at the beginning menu->addAction(action_robotpilot); // add action at the end qDebug() << "Done"; @@ -116,12 +144,12 @@ bool PluginExample::PluginItemClick(Item item, QMenu *menu, TypeClick click_type return false; } -QString PluginExample::PluginCommand(const QString &command, const QString &value){ +QString PluginExample::PluginCommand(const QString &command, const QString &value) { qDebug() << "Sent command: " << command << " With value: " << value; - if (command.compare("Information", Qt::CaseInsensitive) == 0){ - callback_information(); + if (command.compare("BenchmarkInfo", Qt::CaseInsensitive) == 0) { + callback_benchmarkInfo(value); return "Done"; - } else if (command.compare("RobotPilot", Qt::CaseInsensitive) == 0){ + } else if (command.compare("RobotPilot", Qt::CaseInsensitive) == 0) { callback_robotpilot(); return "Done"; } @@ -312,84 +340,303 @@ void PluginExample::PluginEvent(TypeEvent event_type) { //---------------------------------------------------------------------------------- // Define your own button callbacks here -void PluginExample::callback_information(){ +// One row of the benchmark report: either a plain metric/value pair, or (if is_section is set) +// a full-width section title. Kept as data so the same rows can be rendered both as an HTML +// table (for the dock widget) and as an aligned plain-text table (for the console). +struct BenchmarkRow { + QString metric; + QString value; + bool is_section = false; +}; + +// Formats one row of the benchmark table (metric name + measured value) +static QString BenchmarkRowHtml(const QString &metric, const QString &value) { + return QString("%1" + "%2") + .arg(metric.toHtmlEscaped(), value.toHtmlEscaped()); +} + +// Returns 2-3 benchmark rows describing the System (OS), CPU (model, cores, frequency) and RAM of this computer. +// The CPU model/frequency and total RAM require a few OS-specific calls since Qt does not +// expose them directly; any piece that is not available on this platform (e.g. CPU frequency +// on Apple Silicon Macs) is simply left out instead of failing. +static QVector HardwareInfoRows() { + QString cpu_model; + QString cpu_freq; + QString ram_total; + +#if defined(Q_OS_WIN) + QSettings cpu_key(R"(HKEY_LOCAL_MACHINE\HARDWARE\DESCRIPTION\System\CentralProcessor\0)", QSettings::NativeFormat); + cpu_model = cpu_key.value("ProcessorNameString").toString().trimmed(); + int mhz = cpu_key.value("~MHz").toInt(); + if (mhz > 0) { + cpu_freq = QString("%1 GHz").arg(mhz / 1000.0, 0, 'f', 2); + } + + MEMORYSTATUSEX mem_status; + mem_status.dwLength = sizeof(mem_status); + if (GlobalMemoryStatusEx(&mem_status)) { + ram_total = QString("%1 GB").arg(mem_status.ullTotalPhys / (1024.0 * 1024.0 * 1024.0), 0, 'f', 1); + } + +#elif defined(Q_OS_MACOS) + char cpu_brand[256] = {}; + size_t cpu_brand_size = sizeof(cpu_brand); + if (sysctlbyname("machdep.cpu.brand_string", cpu_brand, &cpu_brand_size, nullptr, 0) == 0) { + cpu_model = QString::fromLocal8Bit(cpu_brand); + } + + qint64 freq_hz = 0; + size_t freq_size = sizeof(freq_hz); + // Not available on Apple Silicon Macs: sysctlbyname() fails here and cpu_freq stays empty + if (sysctlbyname("hw.cpufrequency", &freq_hz, &freq_size, nullptr, 0) == 0 && freq_hz > 0) { + cpu_freq = QString("%1 GHz").arg(freq_hz / 1.0e9, 0, 'f', 2); + } + + qint64 ram_bytes = 0; + size_t ram_size = sizeof(ram_bytes); + if (sysctlbyname("hw.memsize", &ram_bytes, &ram_size, nullptr, 0) == 0) { + ram_total = QString("%1 GB").arg(ram_bytes / (1024.0 * 1024.0 * 1024.0), 0, 'f', 1); + } + +#elif defined(Q_OS_LINUX) + QFile cpuinfo("/proc/cpuinfo"); + if (cpuinfo.open(QIODevice::ReadOnly | QIODevice::Text)) { + const QStringList lines = QString::fromLocal8Bit(cpuinfo.readAll()).split('\n'); + for (const QString &line : lines) { + if (cpu_model.isEmpty() && line.startsWith("model name")) { + cpu_model = line.section(':', 1).trimmed(); + } else if (cpu_freq.isEmpty() && line.startsWith("cpu MHz")) { + cpu_freq = QString("%1 GHz").arg(line.section(':', 1).trimmed().toDouble() / 1000.0, 0, 'f', 2); + } + } + } + + long n_pages = sysconf(_SC_PHYS_PAGES); + long page_size = sysconf(_SC_PAGE_SIZE); + if (n_pages > 0 && page_size > 0) { + ram_total = QString("%1 GB").arg((double(n_pages) * page_size) / (1024.0 * 1024.0 * 1024.0), 0, 'f', 1); + } +#endif + + if (cpu_model.isEmpty()) { + cpu_model = QSysInfo::currentCpuArchitecture(); + } + + QString cpu_value = QString("%1, %2 cores").arg(cpu_model, QString::number(QThread::idealThreadCount())); + if (!cpu_freq.isEmpty()) { + cpu_value += QString(" @ %1").arg(cpu_freq); + } + + QVector rows; + rows.append({"System", QSysInfo::prettyProductName()}); + rows.append({"CPU", cpu_value}); + if (!ram_total.isEmpty()) { + rows.append({"RAM", ram_total}); + } + return rows; +} + +// Formats a full-width section header row inside the benchmark table (keeps everything in one +// table so both columns stay the same width instead of each table sizing itself independently) +static QString BenchmarkSectionRowHtml(const QString &title) { + return QString("%1") + .arg(title.toHtmlEscaped()); +} + +// Wraps the accumulated rows into a complete Metric/Value HTML table (for the dock widget) +static QString BenchmarkTableHtml(const QVector &rows) { + QString html_rows; + for (const BenchmarkRow &row : rows) { + html_rows += row.is_section ? BenchmarkSectionRowHtml(row.metric) : BenchmarkRowHtml(row.metric, row.value); + } + return QString("" + "" + "" + "") + + html_rows + "
MetricValue
"; +} + +// Formats the accumulated rows as an aligned, human-readable plain-text table (for console output) +static QString BenchmarkTableText(const QVector &rows) { + const int metric_width = 40; + QString text = QString("%1 Value\n").arg(QString("Metric").leftJustified(metric_width)); + text += QString(metric_width + 10, QChar('-')) + "\n"; + for (const BenchmarkRow &row : rows) { + if (row.is_section) { + text += QString("\n-- %1 --\n").arg(row.metric); + } else { + text += QString("%1 %2\n").arg(row.metric.leftJustified(metric_width), row.value); + } + } + return text; +} + +void PluginExample::callback_benchmarkInfo(const QString &progname) { + static QDockWidget *dockedInfo = nullptr; + if (dockedInfo != nullptr) { + dockedInfo->close(); + QApplication::processEvents(); // force the dock widget to be removed + } // Perform some timing tests using the RoboDK API RDK->ShowMessage("Starting timing tests", false); - QString text_message_html("Plugin Timing Tests Summary on " + QDateTime::currentDateTime().toString() + ":
"); - int ntests=10000; - //Item robot = RDK->getItem("", IItem::ITEM_TYPE_ROBOT); - Item robot = RDK->ItemUserPick("Pick a robot", IItem::ITEM_TYPE_ROBOT); - if (ItemValid(robot)){ + QString header_html; + header_html += "

Plugin Timing Tests Summary

"; + header_html += QString("

%1

").arg(QDateTime::currentDateTime().toString()); + + QString text_message_html = header_html; + + // Create the report widget now: this lets us show partial results while the (potentially slow) collision checks run below + QTextEdit *text_editor = new QTextEdit(); + text_editor->setReadOnly(true); + text_editor->setHtml(text_message_html); + + dockedInfo = AddDockWidget(MainWindow, text_editor, "Dock Plugin timing summary"); + + // Declared here (rather than inside the if-block below) so the rows collected so far are still + // available afterwards to print the plain-text console report + QVector benchmark_rows; + + const int ntests = 10000; + Item robot = RDK->ItemUserPick("Select a robot arm", IItem::ITEM_TYPE_ROBOT_ARM); + if (ItemValid(robot)) { Mat pose_fk; tJoints joints_ik; QList joints_ik_all; - qint64 tstart; - qint64 tend; + QElapsedTimer timer; - text_message_html += + "
" + QString("Using robot %1").arg(robot->Name()); + benchmark_rows.append({"Robot", robot->Name()}); + benchmark_rows += HardwareInfoRows(); - // Test Forward Kinematics - tstart = QDateTime::currentMSecsSinceEpoch(); - for (int i=0; iSolveFK(robot->Joints()); } - tend = QDateTime::currentMSecsSinceEpoch(); - text_message_html += + "
" + QString("Forward Kinematics: %1 micro seconds").arg(((double)(tend-tstart)*1000)/ntests, 0, 'f', 2); + benchmark_rows.append({"Forward Kinematics", QString("%1 microseconds").arg((1e-3 * timer.nsecsElapsed()) / ntests, 0, 'f', 2)}); // Test Inverse Kinematics - tstart = QDateTime::currentMSecsSinceEpoch(); - for (int i=0; iSolveIK(pose_fk); } - tend = QDateTime::currentMSecsSinceEpoch(); - text_message_html += "
" + QString("Inverse Kinematics: %1 micro seconds").arg(((double)(tend-tstart)*1000)/ntests, 0, 'f', 2); + benchmark_rows.append({"Inverse Kinematics", QString("%1 microseconds").arg((1e-3 * timer.nsecsElapsed()) / ntests, 0, 'f', 2)}); - // Test Forward Kinematics - tstart = QDateTime::currentMSecsSinceEpoch(); - for (int i=0; iSolveIK_All(pose_fk); } - tend = QDateTime::currentMSecsSinceEpoch(); - text_message_html += "
" + QString("Inverse Kinematics: %1 micro seconds (all solutions)").arg(((double)(tend-tstart)*1000)/ntests, 0, 'f', 2); - } else { - text_message_html += + "
No robot available to run Kinematic tests"; - } - - - // output through debug console - qDebug() << text_message_html; - - - RDK->ShowMessage("Retrieving all station items", false); - QStringList item_list_names = RDK->getItemListNames(); - qDebug() << "Available items in the current station: " << item_list_names; - - QList item_list = RDK->getItemList(); - - RDK->ShowMessage("Displaying list of station items", false); - text_message_html += QString("
Open station %1 items:").arg(RDK->getActiveStation()->Name()); - foreach (Item itm, item_list){ - Item item_parent = itm->Parent(); - if (!ItemValid(item_parent)){ - // station items do not have a parent - text_message_html += QString("
%1 (station)").arg(itm->Name()); - } else { - text_message_html += QString("
%1 -> parent: %2").arg(itm->Name()).arg(item_parent->Name()); + benchmark_rows.append({"Inverse Kinematics (all solutions)", QString("%1 microseconds").arg((1e-3 * timer.nsecsElapsed()) / ntests, 0, 'f', 2)}); + + // Test collisions for each inverse kinematics solution: fewer samples, but a more accurate timer (nanosecond accuracy) + RDK->Collisions(); // Run once first: the first call needs extra bookkeeping for all loaded objects if collision checking was not already on + timer.start(); + int nJoints = joints_ik_all.length(); + int nWithCollisions = 0; + int nWithoutCollisions = 0; + for (int i = 0; i < nJoints; i++) { + robot->setJoints(joints_ik_all.at(i)); + RDK->Render(IRoboDK::RenderUpdateOnly); + int nCollisions = RDK->Collisions(); + if (nCollisions > 0) { + nWithCollisions++; + } else { + nWithoutCollisions++; + } + } + double ms_collisions = (1e-6 * timer.nsecsElapsed()) / nJoints; + double samples_x_sec = 1000.0 / ms_collisions; + qDebug() << "ms per collision: " << ms_collisions; + qDebug() << "Collision samples per second: " << samples_x_sec; + + benchmark_rows.append({QString("Collision check (%1 samples)").arg(nJoints), QString("%1 ms/sample").arg(ms_collisions, 0, 'f', 2)}); + benchmark_rows.append({"Collision check rate", QString("%1 samples/sec").arg(samples_x_sec, 0, 'f', 2)}); + benchmark_rows.append({"Points with collisions", QString::number(nWithCollisions)}); + benchmark_rows.append({"Points without collisions", QString::number(nWithoutCollisions)}); + + // Show the table now: the program collision check below can take a while for long programs + text_message_html = header_html + BenchmarkTableHtml(benchmark_rows); + text_editor->setHtml(text_message_html); + QApplication::processEvents(); // force the dock widget to repaint now, before the (possibly slow) program collision check below + + // Test collisions along the joint list of a full program, using the same metrics as above (optional: the user can cancel this step) + Item program = RDK->getItem(progname.isEmpty() ? "Main" : progname, IItem::ITEM_TYPE_PROGRAM); + if (!ItemValid(program)) { + program = RDK->ItemUserPick("Select a program to check for collisions (optional)", IItem::ITEM_TYPE_PROGRAM); + QApplication::processEvents(); // hides the popup right away as we are doing heavy processing afterwards + } + if (ItemValid(program)) { + // Section header row for the program results, added to the same table so both columns stay the same width + benchmark_rows.append({QString("Program Collision Check: %1").arg(program->Name()), QString(), true}); + + RDK->ShowMessage("Calculating collisions for program: " + program->Name() + " ...", false); + tMatrix2D *list_joints = Matrix2D_Create(); + QString err_msg; + int result = program->InstructionListJoints(err_msg, list_joints, 1, 1, IRoboDK::COLLISION_OFF); + + if (result >= 0) { + int nDOFs = robot->Joints().Length(); + int nSteps = Matrix2D_Get_ncols(list_joints); + int nProgWithCollisions = 0; + int nProgWithoutCollisions = 0; + + timer.start(); + for (int col = 0; col < nSteps; col++) { + // Each column of the matrix holds one step: [J1..Jn, ERROR, MM_STEP, DEG_STEP, MOVE_ID] + tJoints step_joints(list_joints, col, nDOFs); + robot->setJoints(step_joints); + RDK->Render(IRoboDK::RenderUpdateOnly); + int nCollisions = RDK->Collisions(); + if (nCollisions > 0) { + nProgWithCollisions++; + } else { + nProgWithoutCollisions++; + } + // Update the progress from time to time: + if (col % 100 == 0){ + RDK->Command("ProgressBar", QString("%1").arg(100.0*col/nSteps)); + } + } + RDK->Command("ProgressBar", "-1"); + double ms_prog_collisions = (1e-6 * timer.nsecsElapsed()) / nSteps; + double prog_samples_x_sec = 1000.0 / ms_prog_collisions; + + benchmark_rows.append({QString("Collision check (%1 steps)").arg(nSteps), QString("%1 ms/step").arg(ms_prog_collisions, 0, 'f', 2)}); + benchmark_rows.append({"Collision check rate", QString("%1 samples/sec").arg(prog_samples_x_sec, 0, 'f', 2)}); + benchmark_rows.append({"Points with collisions", QString::number(nProgWithCollisions)}); + benchmark_rows.append({"Points without collisions", QString::number(nProgWithoutCollisions)}); + } else { + qDebug() << "InstructionListJoints failed: " << err_msg; + benchmark_rows.append({"Collision check", tr("Failed: %1").arg(err_msg)}); + } + + ::Matrix2D_Delete(&list_joints); + + // Update the report now that the program collision check is done (same table, so columns stay aligned) + text_message_html = header_html + BenchmarkTableHtml(benchmark_rows); + text_editor->setHtml(text_message_html); } + } else { + text_message_html = header_html + "

No robot available to run Kinematic tests

"; + text_editor->setHtml(text_message_html); } + // Print the same results as an aligned, human-readable plain-text table in the console + if (!benchmark_rows.isEmpty()) { + qDebug().noquote() << "\n" + BenchmarkTableText(benchmark_rows); + } else { + qDebug() << "No robot available to run Kinematic tests"; + } - QTextEdit *text_editor = new QTextEdit("Plugin timing summary"); - text_editor->setHtml(text_message_html); - - AddDockWidget(MainWindow, text_editor, "Dock Plugin timing summary"); + RDK->ShowMessage("Done with benchmark calculation", false); } -void PluginExample::callback_robotpilot(){ - if (dock_robotpilot != nullptr){ +void PluginExample::callback_robotpilot() { + if (dock_robotpilot != nullptr) { // prevent opening more than 1 form RDK->ShowMessage("Robot pilot form is already open", false); return; @@ -399,14 +646,14 @@ void PluginExample::callback_robotpilot(){ dock_robotpilot = AddDockWidget(MainWindow, form_robotpilot, "Robot Pilot"); connect(form_robotpilot, SIGNAL(destroyed()), this, SLOT(callback_robotpilot_closed())); } -void PluginExample::callback_robotpilot_closed(){ + +void PluginExample::callback_robotpilot_closed() { // it is important to reset pointers when the form is closed (deleted) dock_robotpilot = nullptr; form_robotpilot = nullptr; RDK->ShowMessage("Closed robot pilot", false); } -void PluginExample::callback_help(){ + +void PluginExample::callback_help() { QDesktopServices::openUrl(QUrl("https://robodk.com/CreatePlugin")); } - - diff --git a/PluginExample/pluginexample.h b/PluginExample/pluginexample.h index ad20961..75772eb 100644 --- a/PluginExample/pluginexample.h +++ b/PluginExample/pluginexample.h @@ -1,15 +1,12 @@ #ifndef PLUGINEXAMPLE_H #define PLUGINEXAMPLE_H - #include #include #include #include "iapprobodk.h" #include "robodktypes.h" - - class QToolBar; class QMenu; class QAction; @@ -26,19 +23,19 @@ class FormRobotPilot; class PluginExample : public QObject, IAppRoboDK { Q_OBJECT - Q_PLUGIN_METADATA(IID "RoboDK.IAppRoboDK")// FILE "metadatalugin.json") + Q_PLUGIN_METADATA(IID "RoboDK.IAppRoboDK") Q_INTERFACES(IAppRoboDK) public: //------------------------------- RoboDK Plug-in Interface commands ------------------------------ - QString PluginName(void) override; - virtual QString PluginLoad(QMainWindow *mw, QMenuBar *menubar, QStatusBar *statusbar, RoboDK *rdk, const QString &settings="") override; - virtual void PluginUnload() override; - virtual void PluginLoadToolbar(QMainWindow *mw, int icon_size) override; - virtual bool PluginItemClick(Item item, QMenu *menu, TypeClick click_type) override; - virtual QString PluginCommand(const QString &command, const QString &value) override; - virtual void PluginEvent(TypeEvent event_type) override; + QString PluginName() override; + QString PluginLoad(QMainWindow *mw, QMenuBar *menubar, QStatusBar *statusbar, RoboDK *rdk, const QString &settings="") override; + void PluginUnload() override; + void PluginLoadToolbar(QMainWindow *mw, int icon_size) override; + bool PluginItemClick(Item item, QMenu *menu, TypeClick click_type) override; + QString PluginCommand(const QString &command, const QString &value) override; + void PluginEvent(TypeEvent event_type) override; //---------------------------------------------------------------------------------- @@ -56,8 +53,9 @@ class PluginExample : public QObject, IAppRoboDK public slots: // define button callbacks (or slots) here. They are triggered automatically when the button is selected. - /// Called when the information button/action is selected - void callback_information(); + /// Called when the information button/action is selected, or via PluginCommand("BenchmarkInfo", progname) + /// \param progname Name of the program to check for collisions. Defaults to "Main" when left empty. + void callback_benchmarkInfo(const QString &progname = QString()); /// Called when the robot pilot button/action is selected void callback_robotpilot(); @@ -65,7 +63,7 @@ public slots: /// Called when the robot pilot window is closed (event triggered by the dock window) void callback_robotpilot_closed(); - /// Called when the user select the button/action for help + /// Called when the user selects the button/action for help void callback_help(); // define your actions: usually, one action per button @@ -76,8 +74,8 @@ public slots: /// Pointer to the customized menu QMenu *menu1; - /// Information action. callback_information is triggered with this action. Actions are required to populate toolbars and menus and allows getting callbacks. - QAction *action_information; + /// Benchmark info action. callback_benchmarkInfo is triggered with this action. Actions are required to populate toolbars and menus and allows getting callbacks. + QAction *action_benchmarkInfo; /// Open robot pilot form action. callback_robotpilot is triggered with this action. Actions are required to populate toolbars and menus and allows getting callbacks. QAction *action_robotpilot; diff --git a/PluginExample/run_api_benchmark.py b/PluginExample/run_api_benchmark.py new file mode 100644 index 0000000..b3705b8 --- /dev/null +++ b/PluginExample/run_api_benchmark.py @@ -0,0 +1,145 @@ +# Run some performance tests of RoboDK using the RoboDK API for Python +# Note: for best performance you should use the API through the plug-in interface. Check the README.md file to learn more. + +# You can also use the new version of the API: +from robodk import robolink # RoboDK API +# from robodk import robomath # Robot toolbox +import random +import time +import platform +import os + + + +robolink.import_install("psutil") + +# Detailed CPU frequency/usage and RAM stats need psutil (pip install psutil). +# Without it we still report what the standard library exposes (CPU name and logical core count). +try: + import psutil + HAS_PSUTIL = True +except ImportError: + HAS_PSUTIL = False + + +def print_hardware_info(): + """Print CPU and RAM information about this PC.""" + print("---- Hardware information ----") + print("OS: %s %s (%s)" % (platform.system(), platform.release(), platform.machine())) + print("CPU: %s" % (platform.processor() or platform.machine())) + print("CPU cores (logical): %s" % os.cpu_count()) + + if HAS_PSUTIL: + print("CPU cores (physical): %s" % psutil.cpu_count(logical=False)) + try: + # Not implemented by psutil on some platforms (e.g. Apple Silicon Macs) + freq = psutil.cpu_freq() + except Exception: + freq = None + if freq: + print("CPU frequency: %.0f MHz (max: %.0f MHz)" % (freq.current, freq.max)) + print("CPU usage: %.1f %%" % psutil.cpu_percent(interval=0.1)) + mem = psutil.virtual_memory() + print("RAM total: %.2f GB" % (mem.total / (1024 ** 3))) + print("RAM available: %.2f GB (%.1f %% used)" % (mem.available / (1024 ** 3), mem.percent)) + else: + print("Tip: install 'psutil' (pip install psutil) to also report CPU frequency/usage and RAM size.") + print("-------------------------------") + + +print_hardware_info() + +# Start the RoboDK API +RDK = robolink.Robolink() + +robot = RDK.Item("", robolink.ITEM_TYPE_ROBOT_ARM) + +print("Using robot: %s" % robot.Name()) + +# Retrieve the joint limits as a list to retrieve random sets of robot joints +j_low, j_high, _ = robot.JointLimits() +j_low = j_low.list() +j_high = j_high.list() +def joints_rand(): + jnts = [random.uniform(l, h) for l, h in zip(j_low, j_high)] + return jnts + + +def run_benchmark(name, func, n_tests=10000): + """Run func() n_tests times and print the average time per call, in microseconds.""" + t_start = time.perf_counter() + for _ in range(n_tests): + func() + t_end = time.perf_counter() + t_avg_us = (t_end - t_start) * 1e6 / n_tests + print("%s: %.3f microseconds/call (%d samples)" % (name, t_avg_us, n_tests)) + return t_avg_us + + +def solve_fk_once(): + """Compute the forward kinematics for a random set of joints. Some random joints + can be unreachable/singular and make SolveFK raise an exception: skip those samples.""" + try: + return robot.SolveFK(joints_rand()) + except Exception: + return None + + +def solve_ik_once(pose): + """Compute the inverse kinematics for a given pose. Some poses can be unreachable + and make SolveIK raise an exception: skip those samples.""" + try: + return robot.SolveIK(pose) + except Exception: + return None + + +print("\n---- Kinematics performance ----") + +# Test the SolveFK function (robot.SolveFK on random joints) +run_benchmark("Forward Kinematics (SolveFK)", solve_fk_once) + +# Test the SolveIK function (robot.SolveIK on random joints) +pose_fk = robot.SolveFK(joints_rand()) +run_benchmark("Inverse Kinematics (SolveIK)", lambda: solve_ik_once(pose_fk)) + + +# Test collisions: run for a fixed amount of time and collect statistics +print("\n---- Collision check performance ----") + +t_start = time.perf_counter() +t_check = t_start + 4 +count = 0 +n_with_collision = 0 +n_without_collision = 0 + +# Turn off automatic rendering (usualy immediately after a call that updates the scene) +RDK.Render(False) +# Turn collisions off on each setJoints, we'll retrieve the collision status using Collisions() +RDK.setCollisionActive(robolink.COLLISION_OFF) +while time.perf_counter() < t_check: + joints = joints_rand() + robot.setJoints(joints) + + # Provokes an update to refresh the position of every robot, tool, object in the scene without rendering + RDK.Update() + + # Calculate collisions and collect how many samples were in collision vs not + n_collisions = RDK.Collisions() + if n_collisions > 0: + n_with_collision += 1 + else: + n_without_collision += 1 + + count = count + 1 + +t_end = time.perf_counter() + +# Show metrics +t_delta = t_end - t_start +print("Total samples: %d" % count) +print("Total time: %.3f s" % t_delta) +print("Collision check time: %.3f ms/sample" % ((t_delta / count) * 1e3)) +print("Collision check throughput: %.2f samples/sec" % (count / t_delta)) +print("Samples with collisions (nCollisions>0): %d" % n_with_collision) +print("Samples without collisions (nCollisions<=0): %d" % n_without_collision) diff --git a/PluginExample/run_plugin_benchmark.py b/PluginExample/run_plugin_benchmark.py new file mode 100644 index 0000000..2e3a9d3 --- /dev/null +++ b/PluginExample/run_plugin_benchmark.py @@ -0,0 +1,122 @@ +"""Runs the PluginExample benchmark headlessly against a sample RoboDK station. + +This script: + 1. Downloads a sample station (.rdk) into a temp folder, skipping the download if it is + already there. + 2. Starts a new, headless RoboDK instance. + 3. Makes sure PluginExample is loaded. + 4. Opens the downloaded station. + 5. Triggers the plugin's "BenchmarkInfo" command for a given program. + 6. Prints the resulting benchmark report, which RoboDK streams to its own console/stdout + (Robolink automatically relays it to this script's stdout). + +Usage: + python run_plugin_benchmark.py +""" + +import os +import sys +import tempfile +import urllib.request + +from robodk import robolink + +STATION_URL = "https://cdn.robodk.com/downloads-library/library-stations/Welding-with-Comau-Smart5-NJ-130-2-6.rdk" +PLUGIN_NAME = "PluginExample" # Matches PluginExample.pro's TARGET, used to load the plugin +PLUGIN_ID = "Example Plugin" # Matches PluginExample::PluginName(), used to target PluginCommand +PROGRAM_NAME = "main" # Program to run the collision check against + +from importlib.metadata import version, PackageNotFoundError + +MIN_ROBODK_VERSION = "6.0.0" + + +def check_robodk_version(min_version=MIN_ROBODK_VERSION): + """Make sure the installed robodk package meets the minimum required version.""" + try: + installed_version = version("robodk") + except PackageNotFoundError: + sys.exit("Could not find the 'robodk' package version. Run: pip install --upgrade robodk") + + installed_tuple = tuple(int(part) for part in installed_version.split(".")[:3]) + min_tuple = tuple(int(part) for part in min_version.split(".")[:3]) + if installed_tuple < min_tuple: + sys.exit("This script requires robodk >= %s (found %s). This is needed to show the results in the console. Run: pip install --upgrade robodk" % (min_version, installed_version)) + + print("robodk package version: %s (OK, >= %s)" % (installed_version, min_version)) + + +check_robodk_version() + + +def download_station(url: str) -> str: + """Download a station file to a temp folder. Skips the download if it is already there.""" + temp_dir = os.path.join(tempfile.gettempdir(), "robodk_benchmark") + os.makedirs(temp_dir, exist_ok=True) + + filepath = os.path.join(temp_dir, os.path.basename(url)) + if os.path.exists(filepath): + print("Station already downloaded: %s" % filepath) + return filepath + + print("Downloading %s ..." % url) + request = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"}) + try: + with urllib.request.urlopen(request) as response, open(filepath, "wb") as f: + f.write(response.read()) + except Exception as e: + # Clean up a partially written file so a retry does not mistake it for a full download + if os.path.exists(filepath): + os.remove(filepath) + sys.exit("Failed to download %s: %s" % (url, e)) + + print("Saved to: %s" % filepath) + return filepath + +def print_custom(txt): + global do_print_stdout + if do_print_stdout: + print(txt) + +def main(): + global do_print_stdout + do_print_stdout = False + + station_path = download_station(STATION_URL) + + # Start a new, headless RoboDK instance. + # -DEBUG is required for qDebug()-based plugin console output (such as the benchmark + # report) to actually be printed; Robolink relays RoboDK's stdout to ours automatically. + print("Starting a new headless RoboDK instance...") + RDK = robolink.Robolink(args=["-NEWINSTANCE", "-NOUI", "-EXIT_LAST_COM", "-SKIPINI", "-Settings=LicenseLoad"], close_std_out=print_custom) + + if RDK.Command("LHasMaint") != "1": + msg = "You need a license to run collision checking benchmarks" + sys.exit(msg) + + print("Loading plugin: %s" % PLUGIN_NAME) + if not RDK.PluginLoad(PLUGIN_NAME): + msg = "Failed to load plugin: %s. Are you using the latest version? Is the plugin properly installed?" % PLUGIN_NAME + sys.exit(msg) + + print("Opening station: %s" % station_path) + station = RDK.AddFile(station_path) + if not station.Valid(): + msg = "Failed to open station: %s" % station_path + sys.exit(msg) + + print("=" * 70) + print("Running BenchmarkInfo=%s (output below is streamed live from RoboDK)" % PROGRAM_NAME) + print("=" * 70) + do_print_stdout = True + result = RDK.PluginCommand(PLUGIN_ID, "BenchmarkInfo", PROGRAM_NAME) + do_print_stdout = False + print("=" * 70) + print("Plugin Command result: %s" % result) + + print("Closing RoboDK...") + RDK.CloseRoboDK() + + +if __name__ == "__main__": + main()