From 2252ff64366b843cde3375b6ff013196170dc2b4 Mon Sep 17 00:00:00 2001 From: Jan Hanca Date: Fri, 10 Oct 2025 15:52:19 +0200 Subject: [PATCH 1/5] Add Worlds to UI Signed-off-by: Jan Hanca --- .../service_discovery.h | 10 ++ src/sim_widget.ui | 63 ++++++++++++- src/simulation_widget.cpp | 94 +++++++++++++++++++ src/simulation_widget.h | 13 ++- src/string_to_keys.h | 14 +++ 5 files changed, 192 insertions(+), 2 deletions(-) diff --git a/include/q_simulation_interfaces/service_discovery.h b/include/q_simulation_interfaces/service_discovery.h index 9259b90..d0f9e0b 100644 --- a/include/q_simulation_interfaces/service_discovery.h +++ b/include/q_simulation_interfaces/service_discovery.h @@ -43,6 +43,10 @@ namespace q_simulation_interfaces SERVICE_STEP_SIMULATION, SERVICE_GET_SIM_STATE, SERVICE_SET_SIM_STATE, + SERVICE_GET_CURRENT_WORLD, + SERVICE_GET_AVAILABLE_WORLDS, + SERVICE_LOAD_WORLD, + SERVICE_UNLOAD_WORLD, ACTION_SIMULATE_STEPS, SUPPORTED_SERVICE_IDL_COUNT }; @@ -75,6 +79,12 @@ namespace q_simulation_interfaces false}, {ServiceType::SERVICE_SET_SIM_STATE, "simulation_interfaces/srv/SetSimulationState", "Set Sim State", false}, + {ServiceType::SERVICE_GET_CURRENT_WORLD, "simulation_interfaces/srv/GetCurrentWorld", "Get Current World", + false}, + {ServiceType::SERVICE_GET_AVAILABLE_WORLDS, "simulation_interfaces/srv/GetAvailableWorlds", + "Get Available Worlds", false}, + {ServiceType::SERVICE_LOAD_WORLD, "simulation_interfaces/srv/LoadWorld", "Load World", false}, + {ServiceType::SERVICE_UNLOAD_WORLD, "simulation_interfaces/srv/UnloadWorld", "Unload World", false}, {ServiceType::ACTION_SIMULATE_STEPS, "simulation_interfaces/action/SimulateSteps", "Simulate Steps", true}}}; diff --git a/src/sim_widget.ui b/src/sim_widget.ui index 6adf3aa..e7e5464 100644 --- a/src/sim_widget.ui +++ b/src/sim_widget.ui @@ -11,7 +11,7 @@ - + 0 @@ -519,6 +519,67 @@ + + + Worlds + + + + + -1 + -1 + 681 + 401 + + + + + + + Get Available Worlds + + + + + + + + + + Load World + + + + + + + Unload World + + + + + + + Current world: + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + true diff --git a/src/simulation_widget.cpp b/src/simulation_widget.cpp index 533c64c..2be9fe8 100644 --- a/src/simulation_widget.cpp +++ b/src/simulation_widget.cpp @@ -114,6 +114,9 @@ namespace q_simulation_interfaces connect(ui_->setSimStateButton, &QPushButton::clicked, this, &SimulationWidget::SetSimulationState); connect(ui_->stepSimServiceButton, &QPushButton::clicked, this, &SimulationWidget::StepSimulationService); connect(ui_->ComboEntities, &QComboBox::currentTextChanged, this, [this]() { this->GetEntityState(true); }); + connect(ui_->getAvailableWorldsButton, &QPushButton::clicked, this, &SimulationWidget::GetAvailableWorlds); + connect(ui_->loadWorldButton, &QPushButton::clicked, this, &SimulationWidget::LoadWorld); + connect(ui_->unloadWorldButton, &QPushButton::clicked, this, &SimulationWidget::UnloadWorld); // Connect spawn position spin boxes to update marker connect(ui_->doubleSpinBoxX, QOverload::of(&QDoubleSpinBox::valueChanged), this, @@ -234,6 +237,97 @@ namespace q_simulation_interfaces setSimulationStateService_->call_service_async(cb, request); } + + void SimulationWidget::LoadWorld() + { + if (!loadWorldService_) + { + QMessageBox::warning(this, "Service Not Available", + "Load World service is not available. Please select a valid service."); + return; + } + + simulation_interfaces::srv::LoadWorld::Request request; + auto selectedWorld = ui_->availableWorldsCombo->currentText(); + request.uri = selectedWorld.toStdString(); + + auto cb = [this](auto response) + { + ProduceWarningIfProblem(this, "Load World", response); + if (response) + { + ui_->currentWorldLabel->setText("Current world: " + QString::fromStdString(response->world)); + } + }; + } + + void SimulationWidget::UnloadWorld() + { + if (!unloadWorldService_) + { + QMessageBox::warning(this, "Service Not Available", + "Unload World service is not available. Please select a valid service."); + return; + } + + simulation_interfaces::srv::UnloadWorld::Request request; + + auto cb = [this](auto response) + { + ProduceWarningIfProblem(this, "Unload World", response); + if (response) + { + ui_->currentWorldLabel->setText("Current world: "); + } + }; + unloadWorldService_->call_service_async(cb, request); + } + + void SimulationWidget::GetAvailableWorlds() + { + if (!getAvailableWorldsService_) + { + QMessageBox::warning(this, "Service Not Available", + "Get Available Worlds service is not available. Please select a valid service."); + return; + } + + simulation_interfaces::srv::GetAvailableWorlds::Request request; + auto cb = [this](auto response) + { + ProduceWarningIfProblem(this, "Get Available Worlds", response); + if (response && response->result.result == simulation_interfaces::msg::Result::RESULT_OK) + { + ui_->availableWorldsCombo->clear(); + + auto worlds = response->worlds; + for (const auto& world : worlds) + { + ui_->availableWorldsCombo->addItem(QString::fromStdString(world.name)); + } + } + }; + getAvailableWorldsService_->call_service_async(cb, request); + } + + void SimulationWidget::GetCurrentWorld() + { + if (!getCurrentWorldService_) + { + return; + } + simulation_interfaces::srv::GetCurrentWorld::Request request; + auto cb = [this](auto response) + { + ProduceWarningIfProblem(this, "Get Current World", response); + if (response && response->result.result == simulation_interfaces::msg::Result::RESULT_OK) + { + ui_->currentWorldLabel->setText("Current world: " + QString::fromStdString(response->world.name)); + } + }; + getCurrentWorldService_->call_service_async(cb, request); + } + void SimulationWidget::ActionThreadWorker(int steps) { actionThreadRunning_.store(true); diff --git a/src/simulation_widget.h b/src/simulation_widget.h index 99e83e1..3df0529 100644 --- a/src/simulation_widget.h +++ b/src/simulation_widget.h @@ -30,15 +30,19 @@ #include #include #include +#include +#include #include #include #include #include +#include #include #include #include #include #include +#include namespace Ui { @@ -65,7 +69,6 @@ namespace q_simulation_interfaces void hideEvent(QHideEvent* event) override; void showEvent(QShowEvent* event) override; - void onShowServicesTab(); void GetSpawnables(); void SpawnButton(); void GetAllEntities(); @@ -78,6 +81,10 @@ namespace q_simulation_interfaces void GetSimulationState(); void SetSimulationState(); void StepSimulationService(); + void LoadWorld(); + void UnloadWorld(); + void GetAvailableWorlds(); + void GetCurrentWorld(); //! The thread with own ROS 2 node that will run the action client void ActionThreadWorker(int steps); @@ -112,6 +119,10 @@ namespace q_simulation_interfaces std::shared_ptr> getSimulationStateService_; std::shared_ptr> setSimulationStateService_; std::shared_ptr> stepSimulationService_; + std::shared_ptr> getAvailableWorldsService_; + std::shared_ptr> getCurrentWorldService_; + std::shared_ptr> loadWorldService_; + std::shared_ptr> unloadWorldService_; // Action names std::string simulateStepsAction_ = ""; diff --git a/src/string_to_keys.h b/src/string_to_keys.h index 4788b71..b21cdae 100644 --- a/src/string_to_keys.h +++ b/src/string_to_keys.h @@ -44,7 +44,14 @@ const std::map FeatureToName{ {31, "STEP_SIMULATION_SINGLE"}, {32, "STEP_SIMULATION_MULTIPLE"}, {33, "STEP_SIMULATION_ACTION"}, + {40, "WORLD_LOADING"}, + {41, "WORLD_RESOURCE_STRING"}, + {42, "WORLD_TAGS"}, + {43, "WORLD_UNLOADING"}, + {44, "WORLD_INFO_GETTING"}, + {45, "AVAILABLE_WORLDS"}, }; + const std::map FeatureDescription{ {0, "Supports spawn interface (SpawnEntity)."}, {1, "Supports deleting entities (DeleteEntity)."}, @@ -71,7 +78,14 @@ const std::map FeatureDescription{ {31, "Supports single stepping through simulation with StepSimulation interface."}, {32, "Supports multi-stepping through simulation, either through StepSimulation service or StepSimulation action."}, {33, "Supports SimulateSteps action interface."}, + {40, "Supports loading worlds through LoadWorld interface."}, + {41, "Supports resource_string field in LoadWorld interface."}, + {42, "Supports tags field in LoadWorld interface."}, + {43, "Supports unloading worlds through UnloadWorld interface."}, + {44, "Supports GetWorldInfo interface."}, + {45, "Supports GetAvailableWorlds interface."}, }; + const std::unordered_map ScopeNameToId{ {"SCOPE_DEFAULT", 0}, {"SCOPE_TIME", 1}, {"SCOPE_STATE", 2}, {"SCOPE_SPAWNED", 4}, {"SCOPE_ALL", 255}, }; From d015f12f5e9e21ba1a9060d0c8e8beaadda15735 Mon Sep 17 00:00:00 2001 From: Jan Hanca Date: Fri, 10 Oct 2025 16:50:02 +0200 Subject: [PATCH 2/5] Fix LoadLevel; fix UI Signed-off-by: Jan Hanca --- src/sim_widget.ui | 104 +++++++++++++++++--------------------- src/simulation_widget.cpp | 54 ++++++++++++++++++-- 2 files changed, 98 insertions(+), 60 deletions(-) diff --git a/src/sim_widget.ui b/src/sim_widget.ui index e7e5464..6014399 100644 --- a/src/sim_widget.ui +++ b/src/sim_widget.ui @@ -14,7 +14,7 @@ - 0 + 3 @@ -523,62 +523,52 @@ Worlds - - - - -1 - -1 - 681 - 401 - - - - - - - Get Available Worlds - - - - - - - - - - Load World - - - - - - - Unload World - - - - - - - Current world: - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - + + + + + Get Available Worlds + + + + + + + + + + Load World + + + + + + + Unload World + + + + + + + Current world: + + + + + + + Qt::Vertical + + + + 20 + 225 + + + + + diff --git a/src/simulation_widget.cpp b/src/simulation_widget.cpp index 2be9fe8..0763bc5 100644 --- a/src/simulation_widget.cpp +++ b/src/simulation_widget.cpp @@ -251,14 +251,18 @@ namespace q_simulation_interfaces auto selectedWorld = ui_->availableWorldsCombo->currentText(); request.uri = selectedWorld.toStdString(); + std::cout << "Loading world: " << request.uri << std::endl; + auto cb = [this](auto response) { ProduceWarningIfProblem(this, "Load World", response); if (response) { - ui_->currentWorldLabel->setText("Current world: " + QString::fromStdString(response->world)); + ui_->currentWorldLabel->setText("Current world: " + + QString::fromStdString(response->world.world_resource.uri)); } }; + loadWorldService_->call_service_async(cb, request); } void SimulationWidget::UnloadWorld() @@ -293,6 +297,8 @@ namespace q_simulation_interfaces } simulation_interfaces::srv::GetAvailableWorlds::Request request; + request.offline_only = true; + auto cb = [this](auto response) { ProduceWarningIfProblem(this, "Get Available Worlds", response); @@ -303,7 +309,7 @@ namespace q_simulation_interfaces auto worlds = response->worlds; for (const auto& world : worlds) { - ui_->availableWorldsCombo->addItem(QString::fromStdString(world.name)); + ui_->availableWorldsCombo->addItem(QString::fromStdString(world.world_resource.uri)); } } }; @@ -322,7 +328,8 @@ namespace q_simulation_interfaces ProduceWarningIfProblem(this, "Get Current World", response); if (response && response->result.result == simulation_interfaces::msg::Result::RESULT_OK) { - ui_->currentWorldLabel->setText("Current world: " + QString::fromStdString(response->world.name)); + ui_->currentWorldLabel->setText("Current world: " + + QString::fromStdString(response->world.world_resource.uri)); } }; getCurrentWorldService_->call_service_async(cb, request); @@ -971,6 +978,47 @@ namespace q_simulation_interfaces serviceInterfaces_.insert(setSimulationStateService_); } break; + case ServiceType::SERVICE_GET_CURRENT_WORLD: + serviceInterfaces_.erase(getCurrentWorldService_); + getCurrentWorldService_ = shouldReset + ? nullptr + : std::make_shared>(selectedServiceName, node_); + if (getCurrentWorldService_) + { + serviceInterfaces_.insert(getCurrentWorldService_); + } + GetCurrentWorld(); + break; + case ServiceType::SERVICE_GET_AVAILABLE_WORLDS: + serviceInterfaces_.erase(getAvailableWorldsService_); + getAvailableWorldsService_ = shouldReset + ? nullptr + : std::make_shared>(selectedServiceName, node_); + if (getAvailableWorldsService_) + { + serviceInterfaces_.insert(getAvailableWorldsService_); + } + break; + case ServiceType::SERVICE_LOAD_WORLD: + serviceInterfaces_.erase(loadWorldService_); + loadWorldService_ = shouldReset + ? nullptr + : std::make_shared>(selectedServiceName, node_); + if (loadWorldService_) + { + serviceInterfaces_.insert(loadWorldService_); + } + break; + case ServiceType::SERVICE_UNLOAD_WORLD: + serviceInterfaces_.erase(unloadWorldService_); + unloadWorldService_ = shouldReset + ? nullptr + : std::make_shared>(selectedServiceName, node_); + if (unloadWorldService_) + { + serviceInterfaces_.insert(unloadWorldService_); + } + break; case ServiceType::ACTION_SIMULATE_STEPS: simulateStepsAction_ = selectedServiceName; break; From 2c2c02d4bcc03adc4006b037dba0c7341fb34282 Mon Sep 17 00:00:00 2001 From: Jan Hanca Date: Fri, 10 Oct 2025 16:52:04 +0200 Subject: [PATCH 3/5] Change default tab Signed-off-by: Jan Hanca --- src/sim_widget.ui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sim_widget.ui b/src/sim_widget.ui index 6014399..b1f3cec 100644 --- a/src/sim_widget.ui +++ b/src/sim_widget.ui @@ -14,7 +14,7 @@ - 3 + 0 From 45b01d89843b43960bcac58953e585e884e2707a Mon Sep 17 00:00:00 2001 From: Jan Hanca Date: Mon, 13 Oct 2025 11:32:15 +0200 Subject: [PATCH 4/5] Add URI/name support; add offline/online switch Signed-off-by: Jan Hanca --- src/sim_widget.ui | 10 ++++++++++ src/simulation_widget.cpp | 33 ++++++++++++++++++++++++++------- src/simulation_widget.h | 3 +++ 3 files changed, 39 insertions(+), 7 deletions(-) diff --git a/src/sim_widget.ui b/src/sim_widget.ui index b1f3cec..0b8334e 100644 --- a/src/sim_widget.ui +++ b/src/sim_widget.ui @@ -531,6 +531,16 @@ + + + + Worlds: Offline only + + + true + + + diff --git a/src/simulation_widget.cpp b/src/simulation_widget.cpp index 0763bc5..28a1e02 100644 --- a/src/simulation_widget.cpp +++ b/src/simulation_widget.cpp @@ -249,9 +249,14 @@ namespace q_simulation_interfaces simulation_interfaces::srv::LoadWorld::Request request; auto selectedWorld = ui_->availableWorldsCombo->currentText(); - request.uri = selectedWorld.toStdString(); - - std::cout << "Loading world: " << request.uri << std::endl; + if (useUriForWorlds_) + { + request.uri = selectedWorld.toStdString(); + } + else + { + request.resource_string = selectedWorld.toStdString(); + } auto cb = [this](auto response) { @@ -297,7 +302,7 @@ namespace q_simulation_interfaces } simulation_interfaces::srv::GetAvailableWorlds::Request request; - request.offline_only = true; + request.offline_only = ui_->worldsUseOfflineCheck->isChecked(); auto cb = [this](auto response) { @@ -307,9 +312,20 @@ namespace q_simulation_interfaces ui_->availableWorldsCombo->clear(); auto worlds = response->worlds; + + if (!worlds.empty()) + { + // Determine if we are using URI or resource string based on the first world + useUriForWorlds_ = !worlds[0].world_resource.uri.empty(); + } + for (const auto& world : worlds) { - ui_->availableWorldsCombo->addItem(QString::fromStdString(world.world_resource.uri)); + + const QString worldStr = useUriForWorlds_ + ? QString::fromStdString(world.world_resource.uri) + : QString::fromStdString(world.world_resource.resource_string); + ui_->availableWorldsCombo->addItem(worldStr); } } }; @@ -328,8 +344,11 @@ namespace q_simulation_interfaces ProduceWarningIfProblem(this, "Get Current World", response); if (response && response->result.result == simulation_interfaces::msg::Result::RESULT_OK) { - ui_->currentWorldLabel->setText("Current world: " + - QString::fromStdString(response->world.world_resource.uri)); + useUriForWorlds_ = !response->world.world_resource.uri.empty(); + const QString worldStr = useUriForWorlds_ + ? QString::fromStdString(response->world.world_resource.uri) + : QString::fromStdString(response->world.world_resource.resource_string); + ui_->currentWorldLabel->setText("Current world: " + worldStr); } }; getCurrentWorldService_->call_service_async(cb, request); diff --git a/src/simulation_widget.h b/src/simulation_widget.h index 3df0529..8ff9dd0 100644 --- a/src/simulation_widget.h +++ b/src/simulation_widget.h @@ -127,6 +127,9 @@ namespace q_simulation_interfaces // Action names std::string simulateStepsAction_ = ""; + // Whether to use URI or resource string for worlds + bool useUriForWorlds_ = true; + // Vector to hold all service interfaces of created services std::set> serviceInterfaces_; QTimer* timer_; //! Timer for periodic updates From 51a644e39075f9562359a02e8018a1d0d8a208a6 Mon Sep 17 00:00:00 2001 From: Jan Hanca Date: Tue, 21 Oct 2025 13:51:26 +0200 Subject: [PATCH 5/5] Code review fixes Signed-off-by: Jan Hanca --- src/simulation_widget.cpp | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/simulation_widget.cpp b/src/simulation_widget.cpp index 28a1e02..07e8495 100644 --- a/src/simulation_widget.cpp +++ b/src/simulation_widget.cpp @@ -261,11 +261,7 @@ namespace q_simulation_interfaces auto cb = [this](auto response) { ProduceWarningIfProblem(this, "Load World", response); - if (response) - { - ui_->currentWorldLabel->setText("Current world: " + - QString::fromStdString(response->world.world_resource.uri)); - } + GetCurrentWorld(); }; loadWorldService_->call_service_async(cb, request); } @@ -284,10 +280,7 @@ namespace q_simulation_interfaces auto cb = [this](auto response) { ProduceWarningIfProblem(this, "Unload World", response); - if (response) - { - ui_->currentWorldLabel->setText("Current world: "); - } + GetCurrentWorld(); }; unloadWorldService_->call_service_async(cb, request); } @@ -336,11 +329,21 @@ namespace q_simulation_interfaces { if (!getCurrentWorldService_) { + // Getting current world is called periodically, so just update the label without showing a message box + ui_->currentWorldLabel->setText("Current world: unknown due to unavailable service"); return; } simulation_interfaces::srv::GetCurrentWorld::Request request; auto cb = [this](auto response) { + // Check for NO_WORLD_LOADED error, which is not an error in this context + if (response && + response->result.result == simulation_interfaces::srv::GetCurrentWorld::Response::NO_WORLD_LOADED) + { + ui_->currentWorldLabel->setText("Current world: No world loaded"); + return; + } + ProduceWarningIfProblem(this, "Get Current World", response); if (response && response->result.result == simulation_interfaces::msg::Result::RESULT_OK) {