From 5608f897b5efcaad2781a638b6efbd7f837bdb13 Mon Sep 17 00:00:00 2001 From: Fabian Paus <fabian.paus@kit.edu> Date: Fri, 15 Nov 2019 09:30:44 +0100 Subject: [PATCH] ArViz: GUI for starting, stopping and inspecting recordings --- .../components/ArViz/ArVizStorage.cpp | 88 +++++++- .../RobotAPI/components/ArViz/ArVizStorage.h | 2 +- .../RobotAPI/gui-plugins/ArViz/ArVizWidget.ui | 192 ++++++++++++++++-- .../ArViz/ArVizWidgetController.cpp | 76 ++++++- .../gui-plugins/ArViz/ArVizWidgetController.h | 5 + source/RobotAPI/interface/ArViz/Component.ice | 2 +- 6 files changed, 342 insertions(+), 23 deletions(-) diff --git a/source/RobotAPI/components/ArViz/ArVizStorage.cpp b/source/RobotAPI/components/ArViz/ArVizStorage.cpp index c1f59cc39..70313c24d 100644 --- a/source/RobotAPI/components/ArViz/ArVizStorage.cpp +++ b/source/RobotAPI/components/ArViz/ArVizStorage.cpp @@ -28,6 +28,8 @@ #include <VirtualRobot/Util/json/json.hpp> +#include <iomanip> + using namespace armarx; @@ -202,6 +204,15 @@ namespace armarx::viz j["lastTimestampInMicroSeconds"] = batch.lastTimestampInMicroSeconds; } + void from_json(nlohmann::json const& j, armarx::viz::RecordingBatchHeader& batch) + { + batch.index = j["index"] ; + batch.firstRevision = j["firstRevision"]; + batch.lastRevision = j["lastRevision"]; + batch.firstTimestampInMicroSeconds = j["firstTimestampInMicroSeconds"]; + batch.lastTimestampInMicroSeconds = j["lastTimestampInMicroSeconds"]; + } + void to_json(nlohmann::json& j, armarx::viz::Recording const& recording) { j["id"] = recording.id; @@ -212,6 +223,16 @@ namespace armarx::viz j["batchHeaders"] = recording.batchHeaders; } + void from_json(nlohmann::json const& j, armarx::viz::Recording& recording) + { + recording.id = j["id"]; + recording.firstRevision = j["firstRevision"]; + recording.lastRevision = j["lastRevision"]; + recording.firstTimestampInMicroSeconds = j["firstTimestampInMicroSeconds"]; + recording.lastTimestampInMicroSeconds = j["lastTimestampInMicroSeconds"]; + j["batchHeaders"].get_to(recording.batchHeaders); + } + } static bool writeCompleteFile(std::string const& filename, @@ -231,6 +252,21 @@ static bool writeCompleteFile(std::string const& filename, return true; } +static std::string readCompleteFile(std::filesystem::path const& path) +{ + FILE* f = fopen(path.string().c_str(), "rb"); + fseek(f, 0, SEEK_END); + long fsize = ftell(f); + fseek(f, 0, SEEK_SET); /* same as rewind(f); */ + + std::string result(fsize, '\0'); + std::size_t read = fread(result.data(), 1, fsize, f); + result.resize(read); + fclose(f); + + return result; +} + void ArVizStorage::recordBatch(std::vector<viz::TimestampedLayerUpdate> const& batch) { if (batch.empty()) @@ -291,18 +327,26 @@ void ArVizStorage::recordBatch(std::vector<viz::TimestampedLayerUpdate> const& b } -std::string armarx::ArVizStorage::startRecording(std::string const& newRecordingID, const Ice::Current&) +std::string armarx::ArVizStorage::startRecording(std::string const& newRecordingPrefix, const Ice::Current&) { { std::unique_lock<std::mutex> lock(recordingMutex); if (recordingMetaData.id.size() > 0) { - ARMARX_WARNING << "Could not start recording with ID " << newRecordingID + ARMARX_WARNING << "Could not start recording with prefix " << newRecordingPrefix << "\nbecause there is already a recording running for the recording ID: " << recordingMetaData.id; return recordingMetaData.id; } + auto t = std::time(nullptr); + auto tm = *std::localtime(&t); + std::ostringstream id; + id << newRecordingPrefix + << '_' + << std::put_time(&tm, "%Y-%m-%d_%H-%M-%S"); + std::string newRecordingID = id.str(); + recordingPath = historyPath / newRecordingID; if (!std::filesystem::exists(recordingPath)) { @@ -331,7 +375,7 @@ std::string armarx::ArVizStorage::startRecording(std::string const& newRecording recordingTask = new RunningTask<ArVizStorage>(this, &ArVizStorage::record); recordingTask->start(); - return recordingMetaData.id; + return ""; } void armarx::ArVizStorage::stopRecording(const Ice::Current&) @@ -349,11 +393,49 @@ void armarx::ArVizStorage::stopRecording(const Ice::Current&) recordBatch(history); recordingMetaData.id = ""; + recordingMetaData.firstRevision = -1; + recordingMetaData.firstTimestampInMicroSeconds = -1; } viz::RecordingSeq armarx::ArVizStorage::getAllRecordings(const Ice::Current&) { viz::RecordingSeq result; + + for (std::filesystem::directory_entry const& entry : std::filesystem::directory_iterator(historyPath)) + { + ARMARX_INFO << "Checking: " << entry.path(); + + if (!entry.is_directory()) + { + continue; + } + + std::filesystem::path recordingFilePath = entry.path() / "recording.json"; + if (!std::filesystem::exists(recordingFilePath)) + { + ARMARX_INFO << "No recording.json found in directory: " << entry.path(); + continue; + } + + try + { + std::string recordingString = readCompleteFile(recordingFilePath); + nlohmann::json recordingJson = nlohmann::json::parse(recordingString); + + viz::Recording recording; + recordingJson.get_to(recording); + + result.push_back(std::move(recording)); + } + catch (std::exception const& ex) + { + ARMARX_WARNING << "Could not parse JSON file: " << recordingFilePath + << "\nReason: " << ex.what(); + continue; + } + } + + return result; } diff --git a/source/RobotAPI/components/ArViz/ArVizStorage.h b/source/RobotAPI/components/ArViz/ArVizStorage.h index 88570480d..56ec77649 100644 --- a/source/RobotAPI/components/ArViz/ArVizStorage.h +++ b/source/RobotAPI/components/ArViz/ArVizStorage.h @@ -109,7 +109,7 @@ namespace armarx // StorageInterface interface viz::LayerUpdates pullUpdatesSince(Ice::Long revision, const Ice::Current&) override; - std::string startRecording(const std::string&, const Ice::Current&) override; + std::string startRecording(std::string const& prefix, const Ice::Current&) override; void stopRecording(const Ice::Current&) override; viz::RecordingSeq getAllRecordings(const Ice::Current&) override; viz::RecordingBatch getRecordingBatch(const std::string&, Ice::Long, const Ice::Current&) override; diff --git a/source/RobotAPI/gui-plugins/ArViz/ArVizWidget.ui b/source/RobotAPI/gui-plugins/ArViz/ArVizWidget.ui index 53178c762..6400791a9 100644 --- a/source/RobotAPI/gui-plugins/ArViz/ArVizWidget.ui +++ b/source/RobotAPI/gui-plugins/ArViz/ArVizWidget.ui @@ -7,7 +7,7 @@ <x>0</x> <y>0</y> <width>631</width> - <height>429</height> + <height>488</height> </rect> </property> <property name="windowTitle"> @@ -126,7 +126,7 @@ <item> <widget class="QLabel" name="label"> <property name="text"> - <string>ID:</string> + <string>Prefix:</string> </property> </widget> </item> @@ -137,30 +137,188 @@ </property> </widget> </item> + <item> + <widget class="QPushButton" name="startRecordingButton"> + <property name="text"> + <string>Start</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="stopRecordingButton"> + <property name="text"> + <string>Stop</string> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer_2"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> </layout> </item> - <item> - <widget class="QPushButton" name="startRecordingButton"> - <property name="text"> - <string>Start</string> - </property> - </widget> - </item> - <item> - <widget class="QPushButton" name="stopRecordingButton"> - <property name="text"> - <string>Stop</string> - </property> - </widget> - </item> </layout> </widget> </item> <item> <widget class="QGroupBox" name="groupBox_3"> <property name="title"> - <string>Replay</string> + <string>Replay Selection</string> </property> + <layout class="QVBoxLayout" name="verticalLayout_4"> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_7"> + <item> + <widget class="QLabel" name="label_4"> + <property name="text"> + <string>All Recordings</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="refreshRecordingsButton"> + <property name="text"> + <string>Refresh</string> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + </layout> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_8"> + <item> + <widget class="QListWidget" name="recordingList"> + <property name="minimumSize"> + <size> + <width>220</width> + <height>0</height> + </size> + </property> + </widget> + </item> + <item> + <widget class="QGroupBox" name="groupBox_4"> + <property name="title"> + <string>Recording Information</string> + </property> + <layout class="QFormLayout" name="formLayout"> + <item row="0" column="0"> + <widget class="QLabel" name="label_3"> + <property name="text"> + <string>Recording ID:</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QLabel" name="recordingIdLabel"> + <property name="text"> + <string>-----</string> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="label_5"> + <property name="text"> + <string>Revision:</string> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QLabel" name="recordingRevisionLabel"> + <property name="text"> + <string>-----</string> + </property> + </widget> + </item> + <item row="2" column="0"> + <widget class="QLabel" name="label_7"> + <property name="text"> + <string>Timestamp:</string> + </property> + </widget> + </item> + <item row="2" column="1"> + <widget class="QLabel" name="recordingTimestampLabel"> + <property name="minimumSize"> + <size> + <width>290</width> + <height>0</height> + </size> + </property> + <property name="text"> + <string>01.01.1970 01:00:00 - 01.01.1970 01:00:00</string> + </property> + </widget> + </item> + <item row="3" column="0"> + <widget class="QLabel" name="label_9"> + <property name="text"> + <string>Duration:</string> + </property> + </widget> + </item> + <item row="3" column="1"> + <widget class="QLabel" name="recordingDurationLabel"> + <property name="text"> + <string>-----</string> + </property> + </widget> + </item> + <item row="4" column="0"> + <widget class="QLabel" name="label_11"> + <property name="text"> + <string>Batches:</string> + </property> + </widget> + </item> + <item row="4" column="1"> + <widget class="QLabel" name="recordingBatchesLabel"> + <property name="text"> + <string>-----</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <spacer name="horizontalSpacer_3"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + </layout> + </item> + </layout> </widget> </item> </layout> diff --git a/source/RobotAPI/gui-plugins/ArViz/ArVizWidgetController.cpp b/source/RobotAPI/gui-plugins/ArViz/ArVizWidgetController.cpp index 03da4e048..ec24b3917 100644 --- a/source/RobotAPI/gui-plugins/ArViz/ArVizWidgetController.cpp +++ b/source/RobotAPI/gui-plugins/ArViz/ArVizWidgetController.cpp @@ -47,6 +47,9 @@ ArVizWidgetController::ArVizWidgetController() connect(widget.startRecordingButton, &QPushButton::clicked, this, &ArVizWidgetController::onStartRecording); connect(widget.stopRecordingButton, &QPushButton::clicked, this, &ArVizWidgetController::onStopRecording); + connect(widget.refreshRecordingsButton, &QPushButton::clicked, this, &ArVizWidgetController::onRefreshRecordings); + connect(widget.recordingList, &QListWidget::currentItemChanged, this, &ArVizWidgetController::onRecordingSelectionChanged); + // We need a callback from the visualizer, when the layers have changed // So we can update the tree accordingly @@ -312,7 +315,7 @@ void ArVizWidgetController::onStartRecording() std::string runningID = storage->startRecording(recordingID); - if (runningID != recordingID) + if (runningID.size() > 0) { std::string message = "There is already a recording running with ID '" + runningID + "'. \n" @@ -329,6 +332,77 @@ void ArVizWidgetController::onStopRecording() storage->stopRecording(); } +void ArVizWidgetController::onRefreshRecordings() +{ + allRecordings = storage->getAllRecordings(); + + std::string currentText; + QListWidgetItem* currentItem = widget.recordingList->currentItem(); + if (currentItem) + { + currentText = currentItem->text().toStdString(); + } + + // TODO: Remember old selected entry and try to restore + widget.recordingList->clear(); + int currentTextIndex = -1; + int index = 0; + for (auto& recording : allRecordings) + { + if (recording.id == currentText) + { + currentTextIndex = index; + } + widget.recordingList->addItem(QString::fromStdString(recording.id)); + ++index; + } + + if (currentTextIndex > 0) + { + widget.recordingList->setCurrentRow(currentTextIndex); + } +} + +void ArVizWidgetController::onRecordingSelectionChanged(QListWidgetItem* current, QListWidgetItem* previous) +{ + std::string selectedID = current->text().toStdString(); + for (auto& recording : allRecordings) + { + if (recording.id == selectedID) + { + widget.recordingIdLabel->setText(QString::fromStdString(recording.id)); + + widget.recordingRevisionLabel->setText(QString::fromStdString( + std::to_string(recording.firstRevision) + + " - " + + std::to_string(recording.lastRevision))); + + IceUtil::Time firstTime = IceUtil::Time::microSeconds(recording.firstTimestampInMicroSeconds); + IceUtil::Time lastTime = IceUtil::Time::microSeconds(recording.lastTimestampInMicroSeconds); + std::string firstTimeString = firstTime.toDateTime(); + std::string lastTimeString = lastTime.toDateTime(); + firstTimeString = firstTimeString.substr(0, firstTimeString.size() - 4); + lastTimeString = lastTimeString.substr(0, lastTimeString.size() - 4); + + widget.recordingTimestampLabel->setText(QString::fromStdString( + firstTimeString + + " - " + + lastTimeString)); + + IceUtil::Time duration = lastTime - firstTime; + int durationSeconds = duration.toSeconds(); + widget.recordingDurationLabel->setText(QString::fromStdString( + std::to_string(durationSeconds) + " s")); + + widget.recordingBatchesLabel->setText(QString::fromStdString( + std::to_string(recording.batchHeaders.size()) + )); + + break; + } + } +} + SoNode* ArVizWidgetController::getScene() { return visualizer.root; diff --git a/source/RobotAPI/gui-plugins/ArViz/ArVizWidgetController.h b/source/RobotAPI/gui-plugins/ArViz/ArVizWidgetController.h index 9c1ea5554..3a7bb09b5 100644 --- a/source/RobotAPI/gui-plugins/ArViz/ArVizWidgetController.h +++ b/source/RobotAPI/gui-plugins/ArViz/ArVizWidgetController.h @@ -124,6 +124,9 @@ namespace armarx void onStartRecording(); void onStopRecording(); + void onRefreshRecordings(); + void onRecordingSelectionChanged(QListWidgetItem* current, QListWidgetItem* previous); + private: Ui::ArVizWidget widget; @@ -133,6 +136,8 @@ namespace armarx armarx::viz::StorageInterfacePrx storage; armarx::viz::CoinVisualizer visualizer; + + viz::RecordingSeq allRecordings; }; } diff --git a/source/RobotAPI/interface/ArViz/Component.ice b/source/RobotAPI/interface/ArViz/Component.ice index 01b5d75e1..198068966 100644 --- a/source/RobotAPI/interface/ArViz/Component.ice +++ b/source/RobotAPI/interface/ArViz/Component.ice @@ -84,7 +84,7 @@ interface StorageInterface { LayerUpdates pullUpdatesSince(long revision); - string startRecording(string id); + string startRecording(string prefix); void stopRecording(); -- GitLab