From 19264915b4d991f8e6133e5e8779a6994159b776 Mon Sep 17 00:00:00 2001 From: linbo <1034003879@qq.com> Date: Wed, 23 Sep 2026 17:25:01 +0800 Subject: [PATCH] feat: improve arm control workflow and Windows stability --- src/main.cpp | 86 +++++++++ src/mainwindow.cpp | 354 ++++++++++++++++++++++++++++++++++-- src/mainwindow.h | 52 ++++++ src/mainwindow_ui.cpp | 378 +++++++++++++++++++++++++++++++++------ src/robotclient.cpp | 40 ----- src/urdfviewerwidget.cpp | 114 ++++++++++-- styles.qss | 48 ++++- 7 files changed, 948 insertions(+), 124 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index 11f7af7..7ca6fb9 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -11,8 +11,94 @@ #include #include +#ifdef Q_OS_WIN +#include +#include + +namespace { + +LONG WINAPI writeCrashDump(EXCEPTION_POINTERS *exceptionPointers) +{ + static LONG handlingCrash = 0; + if (InterlockedCompareExchange(&handlingCrash, 1, 0) != 0) + return EXCEPTION_EXECUTE_HANDLER; + + wchar_t executablePath[MAX_PATH] = {}; + GetModuleFileNameW(nullptr, executablePath, MAX_PATH); + wchar_t *lastSeparator = wcsrchr(executablePath, L'\\'); + if (lastSeparator) + *lastSeparator = L'\0'; + + wchar_t dumpDirectory[MAX_PATH] = {}; + _snwprintf(dumpDirectory, MAX_PATH - 1, L"%ls\\crash_dumps", executablePath); + CreateDirectoryW(dumpDirectory, nullptr); + + SYSTEMTIME time = {}; + GetLocalTime(&time); + wchar_t baseName[96] = {}; + _snwprintf(baseName, 95, + L"CmvrControlStation_%04u%02u%02u_%02u%02u%02u_%lu", + time.wYear, time.wMonth, time.wDay, + time.wHour, time.wMinute, time.wSecond, + GetCurrentProcessId()); + + wchar_t dumpPath[MAX_PATH] = {}; + _snwprintf(dumpPath, MAX_PATH - 1, L"%ls\\%ls.dmp", dumpDirectory, baseName); + HANDLE dumpFile = CreateFileW(dumpPath, GENERIC_WRITE, FILE_SHARE_READ, nullptr, + CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); + if (dumpFile != INVALID_HANDLE_VALUE) { + MINIDUMP_EXCEPTION_INFORMATION exceptionInfo = {}; + exceptionInfo.ThreadId = GetCurrentThreadId(); + exceptionInfo.ExceptionPointers = exceptionPointers; + exceptionInfo.ClientPointers = FALSE; + const MINIDUMP_TYPE dumpType = static_cast( + MiniDumpWithDataSegs | MiniDumpWithHandleData | + MiniDumpWithThreadInfo | MiniDumpWithIndirectlyReferencedMemory); + MiniDumpWriteDump(GetCurrentProcess(), GetCurrentProcessId(), dumpFile, + dumpType, &exceptionInfo, nullptr, nullptr); + FlushFileBuffers(dumpFile); + CloseHandle(dumpFile); + } + + wchar_t summaryPath[MAX_PATH] = {}; + _snwprintf(summaryPath, MAX_PATH - 1, L"%ls\\%ls.txt", dumpDirectory, baseName); + HANDLE summaryFile = CreateFileW(summaryPath, GENERIC_WRITE, FILE_SHARE_READ, nullptr, + CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); + if (summaryFile != INVALID_HANDLE_VALUE) { + const DWORD exceptionCode = exceptionPointers && exceptionPointers->ExceptionRecord + ? exceptionPointers->ExceptionRecord->ExceptionCode : 0; + const void *exceptionAddress = exceptionPointers && exceptionPointers->ExceptionRecord + ? exceptionPointers->ExceptionRecord->ExceptionAddress : nullptr; + char summary[512] = {}; + const int length = _snprintf(summary, sizeof(summary) - 1, + "Application: CmvrControlStation.exe\r\n" + "Timestamp: %04u-%02u-%02u %02u:%02u:%02u\r\n" + "Exception code: 0x%08lX\r\n" + "Exception address: %p\r\n" + "Process ID: %lu\r\nThread ID: %lu\r\n" + "Dump: %ls\r\n", + time.wYear, time.wMonth, time.wDay, + time.wHour, time.wMinute, time.wSecond, + exceptionCode, exceptionAddress, + GetCurrentProcessId(), GetCurrentThreadId(), dumpPath); + DWORD written = 0; + if (length > 0) + WriteFile(summaryFile, summary, static_cast(length), &written, nullptr); + FlushFileBuffers(summaryFile); + CloseHandle(summaryFile); + } + + return EXCEPTION_EXECUTE_HANDLER; +} + +} // namespace +#endif + int main(int argc, char *argv[]) { +#ifdef Q_OS_WIN + SetUnhandledExceptionFilter(writeCrashDump); +#endif QApplication app(argc, argv); app.setApplicationName(QStringLiteral("CMVR Control Station")); app.setOrganizationName(QStringLiteral("CMVR")); diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index e3e1bed..be2e381 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -659,6 +659,7 @@ void MainWindow::updateTelemetry(const QVariantList &joints, const QVariantMap & byName.insert(name, joint); jointPositions.insert(name, joint.value(QStringLiteral("position")).toDouble()); } + m_latestArmJointPositions = jointPositions; if (m_urdfViewer) m_urdfViewer->setJointPositions(jointPositions); @@ -689,10 +690,13 @@ void MainWindow::updateTelemetry(const QVariantList &joints, const QVariantMap & if (!nameItem) continue; const QVariantMap joint = byName.value(nameItem->text()); - if (!joint.isEmpty()) + if (!joint.isEmpty()) { + const double position = joint.value(QStringLiteral("position")).toDouble(); table->item(row, 1)->setText(QStringLiteral("%1°").arg( - qRadiansToDegrees(joint.value(QStringLiteral("position")).toDouble()), + qRadiansToDegrees(position), 0, 'f', 2)); + table->item(row, 2)->setText(QString::number(position, 'f', 6)); + } } } @@ -967,6 +971,19 @@ void MainWindow::handleCommand(const QString &command, bool success, const QStri } appendLog(success ? QStringLiteral("OK") : QStringLiteral("ERROR"), QStringLiteral("%1 · %2").arg(command, detail)); + if (command == QStringLiteral("MoveJ") && m_jointPointMovePending) { + m_jointPointMovePending = false; + if (!success) { + stopJointPointLoop(QStringLiteral("MoveJ 执行失败:%1").arg(detail)); + } else if (m_jointPointLoopActive) { + const int completedIndex = m_jointPointLoopCursor - 1; + const double waitSeconds = completedIndex >= 0 + && completedIndex < m_jointPointExecutionWaits.size() + ? m_jointPointExecutionWaits.at(completedIndex) : 0.0; + const int delayMs = qRound(waitSeconds * 1000.0); + QTimer::singleShot(delayMs, this, &MainWindow::advanceJointPointLoop); + } + } if (!success) m_connectionText->setToolTip(detail); } @@ -1003,13 +1020,260 @@ void MainWindow::sendMoveJ() QVariantList positions; const bool leftArm = m_armSideCombo && m_armSideCombo->currentData().toString() == QStringLiteral("left"); - const int offset = leftArm ? 0 : 7; - for (int i = 0; i < 7 && offset + i < m_targetSpins.size(); ++i) - positions.append(qDegreesToRadians(m_targetSpins.at(offset + i)->value())); + const QList &spins = leftArm + ? m_leftTargetSpins : m_rightTargetSpins; + for (QDoubleSpinBox *spin : spins) + positions.append(qDegreesToRadians(spin->value())); m_client->requestMoveJ(m_armIdEdit->text().trimmed(), positions, m_velocitySpin->value(), m_accelerationSpin->value()); } +void MainWindow::refreshRecordedJointPoints() +{ + if (!m_jointPointTable) + return; + m_jointPointTable->setRowCount(m_recordedJointPoints.size()); + for (int row = 0; row < m_recordedJointPoints.size(); ++row) { + const RecordedJointPoint &point = m_recordedJointPoints.at(row); + QStringList values; + for (double position : point.positions) + values.append(QString::number(position, 'f', 6)); + m_jointPointTable->setItem(row, 0, new QTableWidgetItem(point.name)); + m_jointPointTable->setItem(row, 1, new QTableWidgetItem( + point.side == QStringLiteral("left") ? QStringLiteral("左臂") + : QStringLiteral("右臂"))); + m_jointPointTable->setItem(row, 2, new QTableWidgetItem(point.deviceId)); + m_jointPointTable->setItem(row, 3, new QTableWidgetItem( + QString::number(point.positions.size()))); + QTableWidgetItem *positionsItem = new QTableWidgetItem(values.join(QStringLiteral(", "))); + positionsItem->setToolTip(QStringLiteral("关节顺序:%1\n角度(rad):%2") + .arg(point.jointNames.join(QStringLiteral(", ")), + values.join(QStringLiteral(", ")))); + m_jointPointTable->setItem(row, 4, positionsItem); + } +} + +void MainWindow::refreshJointFlowTable() +{ + if (!m_jointFlowTable) + return; + m_jointFlowTable->setRowCount(m_jointFlowSteps.size()); + for (int row = 0; row < m_jointFlowSteps.size(); ++row) { + const JointFlowStep &step = m_jointFlowSteps.at(row); + m_jointFlowTable->setItem(row, 0, new QTableWidgetItem(QString::number(row + 1))); + m_jointFlowTable->setItem(row, 1, new QTableWidgetItem(step.pointName)); + m_jointFlowTable->setItem(row, 2, new QTableWidgetItem( + QStringLiteral("%1 s").arg(step.waitSeconds, 0, 'f', 1))); + m_jointFlowTable->setItem(row, 3, new QTableWidgetItem( + step.loopGroup.isEmpty() ? QStringLiteral("—") : step.loopGroup)); + m_jointFlowTable->setItem(row, 4, new QTableWidgetItem( + step.loopGroup.isEmpty() ? QStringLiteral("1") + : QString::number(step.loopCount))); + } +} + +void MainWindow::recordCurrentJointPoint() +{ + const QString name = m_jointPointNameEdit + ? m_jointPointNameEdit->text().trimmed() : QString(); + if (name.isEmpty()) { + QMessageBox::warning(this, QStringLiteral("记录关节点位"), + QStringLiteral("请输入动作名称。")); + return; + } + for (const RecordedJointPoint &point : m_recordedJointPoints) { + if (point.name.compare(name, Qt::CaseInsensitive) == 0) { + QMessageBox::warning(this, QStringLiteral("记录关节点位"), + QStringLiteral("动作名称“%1”已存在,请使用其他名称。") + .arg(name)); + return; + } + } + + const bool leftArm = m_armSideCombo + && m_armSideCombo->currentData().toString() == QStringLiteral("left"); + const QStringList jointNames = leftArm ? m_leftArmJointNames : m_rightArmJointNames; + if (jointNames.isEmpty()) { + QMessageBox::warning(this, QStringLiteral("记录关节点位"), + QStringLiteral("当前 URDF 中没有该机械臂的可运动关节。")); + return; + } + + RecordedJointPoint point; + point.name = name; + point.side = leftArm ? QStringLiteral("left") : QStringLiteral("right"); + point.deviceId = m_armIdEdit->text().trimmed(); + point.jointNames = jointNames; + QStringList missing; + for (const QString &jointName : jointNames) { + if (!m_latestArmJointPositions.contains(jointName)) { + missing.append(jointName); + continue; + } + point.positions.append(m_latestArmJointPositions.value(jointName)); + } + if (!missing.isEmpty()) { + QMessageBox::warning(this, QStringLiteral("记录关节点位"), + QStringLiteral("尚未收到以下关节的实时角度:\n%1\n\n请先启动遥测。") + .arg(missing.join(QStringLiteral(", ")))); + return; + } + if (point.deviceId.isEmpty()) { + QMessageBox::warning(this, QStringLiteral("记录关节点位"), + QStringLiteral("当前 Device ID 为空。")); + return; + } + + m_recordedJointPoints.append(point); + refreshRecordedJointPoints(); + QSettings settings; + saveJointPrograms(settings); + m_jointPointNameEdit->clear(); + m_jointPointTable->selectRow(m_recordedJointPoints.size() - 1); + appendLog(QStringLiteral("OK"), + QStringLiteral("已记录动作“%1”,%2 个关节(rad)") + .arg(point.name).arg(point.positions.size())); +} + +void MainWindow::executeSelectedJointPoint() +{ + if (m_jointPointLoopActive || m_jointPointMovePending) + return; + const QModelIndexList selected = m_jointPointTable + ? m_jointPointTable->selectionModel()->selectedRows() : QModelIndexList(); + if (selected.isEmpty()) { + QMessageBox::information(this, QStringLiteral("执行关节点位"), + QStringLiteral("请先选择一个已记录动作。")); + return; + } + const int row = selected.first().row(); + if (row < 0 || row >= m_recordedJointPoints.size()) + return; + const RecordedJointPoint &point = m_recordedJointPoints.at(row); + QVariantList positions; + for (double position : point.positions) + positions.append(position); + m_jointPointMovePending = true; + m_client->requestMoveJ(point.deviceId, positions, + m_velocitySpin->value(), m_accelerationSpin->value()); + if (m_jointPointLoopStatus) + m_jointPointLoopStatus->setText(QStringLiteral("执行:%1").arg(point.name)); +} + +void MainWindow::startJointPointLoop() +{ + if (m_jointPointLoopActive || m_jointPointMovePending) + return; + if (m_jointFlowSteps.isEmpty()) { + QMessageBox::information(this, QStringLiteral("执行自定义流程"), + QStringLiteral("请先向自定义流程中添加步骤。")); + return; + } + m_jointPointLoopRows.clear(); + m_jointPointExecutionWaits.clear(); + const auto pointRow = [this](const QString &name) { + for (int row = 0; row < m_recordedJointPoints.size(); ++row) { + if (m_recordedJointPoints.at(row).name == name) + return row; + } + return -1; + }; + for (int i = 0; i < m_jointFlowSteps.size();) { + const JointFlowStep &step = m_jointFlowSteps.at(i); + if (step.loopGroup.isEmpty()) { + const int row = pointRow(step.pointName); + if (row >= 0) { + m_jointPointLoopRows.append(row); + m_jointPointExecutionWaits.append(step.waitSeconds); + } + ++i; + continue; + } + int end = i + 1; + while (end < m_jointFlowSteps.size() + && m_jointFlowSteps.at(end).loopGroup == step.loopGroup) + ++end; + for (int repeat = 0; repeat < step.loopCount; ++repeat) { + for (int index = i; index < end; ++index) { + const JointFlowStep &groupStep = m_jointFlowSteps.at(index); + const int row = pointRow(groupStep.pointName); + if (row >= 0) { + m_jointPointLoopRows.append(row); + m_jointPointExecutionWaits.append(groupStep.waitSeconds); + } + } + } + i = end; + } + if (m_jointPointLoopRows.isEmpty()) { + QMessageBox::warning(this, QStringLiteral("执行自定义流程"), + QStringLiteral("流程引用的点位已不存在。")); + return; + } + m_jointPointLoopCursor = 0; + m_jointPointLoopTarget = m_jointPointLoopRows.size(); + m_jointPointLoopActive = true; + m_startJointPointLoopButton->setEnabled(false); + m_runJointPointButton->setEnabled(false); + m_deleteJointPointButton->setEnabled(false); + m_recordJointPointButton->setEnabled(false); + m_stopJointPointLoopButton->setEnabled(true); + m_jointPointTable->setEnabled(false); + advanceJointPointLoop(); +} + +void MainWindow::advanceJointPointLoop() +{ + if (!m_jointPointLoopActive || m_jointPointMovePending) + return; + if (m_jointPointLoopCursor >= m_jointPointLoopRows.size()) { + stopJointPointLoop(QStringLiteral("循环执行完成")); + return; + } + const int executionIndex = m_jointPointLoopCursor; + const int row = m_jointPointLoopRows.at(m_jointPointLoopCursor++); + if (row < 0 || row >= m_recordedJointPoints.size()) { + stopJointPointLoop(QStringLiteral("点位列表已发生变化")); + return; + } + const RecordedJointPoint &point = m_recordedJointPoints.at(row); + QVariantList positions; + for (double position : point.positions) + positions.append(position); + m_jointPointMovePending = true; + m_jointPointLoopStatus->setText(QStringLiteral("步骤 %1/%2 · %3") + .arg(executionIndex + 1).arg(m_jointPointLoopRows.size()).arg(point.name)); + m_client->requestMoveJ(point.deviceId, positions, + m_velocitySpin->value(), m_accelerationSpin->value()); +} + +void MainWindow::stopJointPointLoop(const QString &reason) +{ + const bool wasActive = m_jointPointLoopActive; + m_jointPointLoopActive = false; + m_jointPointMovePending = false; + m_jointPointLoopRows.clear(); + m_jointPointExecutionWaits.clear(); + if (m_startJointPointLoopButton) + m_startJointPointLoopButton->setEnabled(true); + if (m_runJointPointButton) + m_runJointPointButton->setEnabled(true); + if (m_deleteJointPointButton) + m_deleteJointPointButton->setEnabled(true); + if (m_recordJointPointButton) + m_recordJointPointButton->setEnabled(true); + if (m_stopJointPointLoopButton) + m_stopJointPointLoopButton->setEnabled(false); + if (m_jointPointTable) + m_jointPointTable->setEnabled(true); + if (m_jointPointLoopStatus) + m_jointPointLoopStatus->setText(reason.isEmpty() ? QStringLiteral("未运行") : reason); + if (wasActive && !reason.isEmpty()) + appendLog(reason == QStringLiteral("循环执行完成") ? QStringLiteral("OK") + : QStringLiteral("WARN"), + reason); +} + void MainWindow::sendMoveJPose() { if (m_moveJPoseSpins.size() != 6 || !m_moveJPoseBaseLinkCombo) @@ -1050,6 +1314,7 @@ void MainWindow::saveSnapshot() void MainWindow::loadSettings() { QSettings settings; + loadJointPrograms(settings); m_hostEdit->setText(settings.value(QStringLiteral("connection/host"), QStringLiteral("192.168.0.28")).toString()); m_hostEdit->setCursorPosition(0); m_portSpin->setValue(settings.value(QStringLiteral("connection/port"), 50052).toInt()); @@ -1063,14 +1328,16 @@ void MainWindow::loadSettings() m_armSideCombo->setCurrentIndex(sideIndex); } const QString defaultArmDeviceId = armSide == QStringLiteral("left") - ? QStringLiteral("eyou_left_arm") : QStringLiteral("eyou_right_arm"); + ? QStringLiteral("left_ethercat_arm") : QStringLiteral("right_ethercat_arm"); QString armDeviceId = settings.value(QStringLiteral("arm/device"), defaultArmDeviceId).toString().trimmed(); - if (armDeviceId == QStringLiteral("left_arm")) { - armDeviceId = QStringLiteral("eyou_left_arm"); + if (armDeviceId == QStringLiteral("left_arm") || + armDeviceId == QStringLiteral("eyou_left_arm")) { + armDeviceId = QStringLiteral("left_ethercat_arm"); } else if (armDeviceId == QStringLiteral("right_arm") || - armDeviceId == QStringLiteral("eyou_arm")) { - armDeviceId = QStringLiteral("eyou_right_arm"); + armDeviceId == QStringLiteral("eyou_arm") || + armDeviceId == QStringLiteral("eyou_right_arm")) { + armDeviceId = QStringLiteral("right_ethercat_arm"); } m_armIdEdit->setText(armDeviceId); m_cameraIdEdit->setText(settings.value(QStringLiteral("camera/device"), QStringLiteral("camera")).toString()); @@ -1130,6 +1397,7 @@ void MainWindow::loadSettings() void MainWindow::saveSettings() { QSettings settings; + saveJointPrograms(settings); settings.setValue(QStringLiteral("connection/host"), m_hostEdit->text().trimmed()); settings.setValue(QStringLiteral("connection/port"), m_portSpin->value()); settings.setValue(QStringLiteral("connection/demo"), m_demoCheck->isChecked()); @@ -1168,6 +1436,72 @@ void MainWindow::saveSettings() : certificatePath); } +void MainWindow::loadJointPrograms(QSettings &settings) +{ + m_recordedJointPoints.clear(); + const int pointCount = settings.beginReadArray(QStringLiteral("arm/jointPoints")); + for (int i = 0; i < pointCount; ++i) { + settings.setArrayIndex(i); + RecordedJointPoint point; + point.name = settings.value(QStringLiteral("name")).toString(); + point.side = settings.value(QStringLiteral("side")).toString(); + point.deviceId = settings.value(QStringLiteral("deviceId")).toString(); + point.jointNames = settings.value(QStringLiteral("jointNames")).toStringList(); + const QVariantList storedPositions = settings.value(QStringLiteral("positions")).toList(); + for (const QVariant &value : storedPositions) + point.positions.append(value.toDouble()); + if (!point.name.isEmpty() && point.jointNames.size() == point.positions.size()) + m_recordedJointPoints.append(point); + } + settings.endArray(); + + m_jointFlowSteps.clear(); + const int flowCount = settings.beginReadArray(QStringLiteral("arm/jointFlow")); + for (int i = 0; i < flowCount; ++i) { + settings.setArrayIndex(i); + JointFlowStep step; + step.pointName = settings.value(QStringLiteral("pointName")).toString(); + step.waitSeconds = settings.value(QStringLiteral("waitSeconds"), 1.0).toDouble(); + step.loopGroup = settings.value(QStringLiteral("loopGroup")).toString(); + step.loopCount = settings.value(QStringLiteral("loopCount"), 1).toInt(); + if (!step.pointName.isEmpty()) + m_jointFlowSteps.append(step); + } + settings.endArray(); + refreshRecordedJointPoints(); + refreshJointFlowTable(); +} + +void MainWindow::saveJointPrograms(QSettings &settings) const +{ + settings.remove(QStringLiteral("arm/jointPoints")); + settings.beginWriteArray(QStringLiteral("arm/jointPoints")); + for (int i = 0; i < m_recordedJointPoints.size(); ++i) { + settings.setArrayIndex(i); + const RecordedJointPoint &point = m_recordedJointPoints.at(i); + QVariantList positions; + for (double value : point.positions) + positions.append(value); + settings.setValue(QStringLiteral("name"), point.name); + settings.setValue(QStringLiteral("side"), point.side); + settings.setValue(QStringLiteral("deviceId"), point.deviceId); + settings.setValue(QStringLiteral("jointNames"), point.jointNames); + settings.setValue(QStringLiteral("positions"), positions); + } + settings.endArray(); + settings.remove(QStringLiteral("arm/jointFlow")); + settings.beginWriteArray(QStringLiteral("arm/jointFlow")); + for (int i = 0; i < m_jointFlowSteps.size(); ++i) { + settings.setArrayIndex(i); + const JointFlowStep &step = m_jointFlowSteps.at(i); + settings.setValue(QStringLiteral("pointName"), step.pointName); + settings.setValue(QStringLiteral("waitSeconds"), step.waitSeconds); + settings.setValue(QStringLiteral("loopGroup"), step.loopGroup); + settings.setValue(QStringLiteral("loopCount"), step.loopCount); + } + settings.endArray(); +} + void MainWindow::setControlsEnabled(bool enabled) { for (QPushButton *button : m_serviceButtons) { diff --git a/src/mainwindow.h b/src/mainwindow.h index 884a5ec..dd98930 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -9,6 +9,7 @@ #include #include #include +#include #include "videoframe.h" @@ -25,6 +26,7 @@ class QLineEdit; class QProgressBar; class QPushButton; class QResizeEvent; +class QSettings; class QSpinBox; class QStackedWidget; class QTableWidget; @@ -73,6 +75,13 @@ private slots: void appendQuicLog(const QString &level, const QString &message); void sendMoveJ(); void sendMoveJPose(); + void recordCurrentJointPoint(); + void executeSelectedJointPoint(); + void startJointPointLoop(); + void stopJointPointLoop(const QString &reason = QString()); + void advanceJointPointLoop(); + void refreshRecordedJointPoints(); + void refreshJointFlowTable(); void saveSnapshot(); private: @@ -89,6 +98,8 @@ private: QWidget *createDiagnosticsPage(); void loadSettings(); void saveSettings(); + void loadJointPrograms(QSettings &settings); + void saveJointPrograms(QSettings &settings) const; void setControlsEnabled(bool enabled); void updateWindowChrome(); void ensureArmModelLoaded(); @@ -155,6 +166,10 @@ private: QTableWidget *m_motionTable = nullptr; QTableWidget *m_leftMotionTable = nullptr; QList m_targetSpins; + QList m_leftTargetSpins; + QList m_rightTargetSpins; + QStringList m_leftArmJointNames; + QStringList m_rightArmJointNames; QStringList m_activeArmJointNames; QTabWidget *m_motionCommandTabs = nullptr; QList m_moveJPoseSpins; @@ -180,6 +195,43 @@ private: QString m_teachSelectedJoint; QString m_pendingUrdfPath; bool m_armModelLoadScheduled = false; + struct RecordedJointPoint { + QString name; + QString side; + QString deviceId; + QStringList jointNames; + QVector positions; + }; + struct JointFlowStep { + QString pointName; + double waitSeconds = 1.0; + QString loopGroup; + int loopCount = 1; + }; + QList m_recordedJointPoints; + QList m_jointFlowSteps; + QHash m_latestArmJointPositions; + QLineEdit *m_jointPointNameEdit = nullptr; + QTableWidget *m_jointPointTable = nullptr; + QLabel *m_jointPointDetailLabel = nullptr; + QSpinBox *m_jointPointLoopCount = nullptr; + QDoubleSpinBox *m_jointPointInterval = nullptr; + QLineEdit *m_jointFlowGroupEdit = nullptr; + QSpinBox *m_jointFlowGroupCount = nullptr; + QTableWidget *m_jointFlowTable = nullptr; + QPushButton *m_recordJointPointButton = nullptr; + QPushButton *m_runJointPointButton = nullptr; + QPushButton *m_deleteJointPointButton = nullptr; + QPushButton *m_startJointPointLoopButton = nullptr; + QPushButton *m_stopJointPointLoopButton = nullptr; + QLabel *m_jointPointLoopStatus = nullptr; + QVector m_jointPointLoopRows; + QVector m_jointPointExecutionWaits; + int m_jointPointLoopCursor = 0; + int m_jointPointLoopsCompleted = 0; + int m_jointPointLoopTarget = 0; + bool m_jointPointLoopActive = false; + bool m_jointPointMovePending = false; QLineEdit *m_cameraIdEdit = nullptr; VideoRenderWidget *m_videoRender = nullptr; diff --git a/src/mainwindow_ui.cpp b/src/mainwindow_ui.cpp index 2279488..07f17c6 100644 --- a/src/mainwindow_ui.cpp +++ b/src/mainwindow_ui.cpp @@ -39,6 +39,7 @@ #include #include #include +#include #include #include #include @@ -143,11 +144,16 @@ QStringList armJointNames(bool leftArm) bool isArmJointForSide(const QString &jointName, bool leftArm) { - const QString upper = jointName.toUpper(); - return leftArm ? (upper.startsWith(QStringLiteral("L_")) - || upper.contains(QStringLiteral("LEFT"))) - : (upper.startsWith(QStringLiteral("R_")) - || upper.contains(QStringLiteral("RIGHT"))); + const QString lower = jointName.toLower(); + const QString sideWord = leftArm ? QStringLiteral("left") : QStringLiteral("right"); + const QString sidePrefix = leftArm ? QStringLiteral("l_") : QStringLiteral("r_"); + return lower.contains(sideWord + QStringLiteral("_arm")) + || lower.contains(sideWord + QStringLiteral("arm")) + || (lower.startsWith(sidePrefix) + && (lower.contains(QStringLiteral("shoulder")) + || lower.contains(QStringLiteral("elbow")) + || lower.contains(QStringLiteral("wrist")) + || lower.contains(QStringLiteral("arm")))); } QWidget *withRpcExplorer(QWidget *commonPage, RobotClient *client, @@ -247,19 +253,15 @@ QWidget *MainWindow::createArmPage() ui.commandPanel->setParent(ui.urdfTab); ui.urdfTabLayout->insertWidget(0, ui.commandPanel); - QSplitter *modelMotionWorkspace = new QSplitter(Qt::Vertical, ui.urdfTab); - modelMotionWorkspace->setObjectName(QStringLiteral("armModelMotionSplitter")); - modelMotionWorkspace->setChildrenCollapsible(false); - modelMotionWorkspace->setHandleWidth(8); + // Qt3D preview is disabled in release builds. Keep the viewer alive and + // hidden so it can still parse the URDF and provide the joint list, while + // giving the complete workspace height to the command/point-flow tables. ui.urdfTabLayout->removeWidget(ui.urdfViewer); - modelMotionWorkspace->addWidget(ui.urdfViewer); - modelMotionWorkspace->addWidget(ui.targetsPanel); - modelMotionWorkspace->setStretchFactor(0, 5); - modelMotionWorkspace->setStretchFactor(1, 4); - modelMotionWorkspace->setSizes({500, 350}); - ui.targetsPanel->setMinimumHeight(360); - ui.targetsPanel->setMaximumHeight(440); - ui.urdfTabLayout->insertWidget(1, modelMotionWorkspace, 1); + ui.urdfViewer->hide(); + ui.targetsPanel->setMinimumHeight(0); + ui.targetsPanel->setMaximumHeight(QWIDGETSIZE_MAX); + ui.targetsPanel->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding); + ui.urdfTabLayout->insertWidget(1, ui.targetsPanel, 1); ui.urdfTabLayout->setStretch(0, 0); ui.urdfTabLayout->setStretch(1, 1); @@ -309,32 +311,251 @@ QWidget *MainWindow::createArmPage() m_motionCommandTabs->setObjectName(QStringLiteral("armMotionCommandTabs")); m_motionCommandTabs->setDocumentMode(true); QWidget *moveJPage = new QWidget(m_motionCommandTabs); - QVBoxLayout *moveJLayout = new QVBoxLayout(moveJPage); + QGridLayout *moveJLayout = new QGridLayout(moveJPage); moveJLayout->setContentsMargins(0, 0, 0, 0); - QSplitter *armTargetTables = new QSplitter(Qt::Horizontal, moveJPage); + moveJLayout->setHorizontalSpacing(12); + moveJLayout->setVerticalSpacing(10); + moveJLayout->setColumnStretch(0, 4); + moveJLayout->setColumnStretch(1, 6); + moveJLayout->setRowStretch(0, 1); + moveJLayout->setRowStretch(1, 1); + QSplitter *armTargetTables = new QSplitter(Qt::Vertical, moveJPage); armTargetTables->setObjectName(QStringLiteral("armTargetTables")); armTargetTables->setChildrenCollapsible(false); QWidget *leftTargets = new QWidget(armTargetTables); + leftTargets->setObjectName(QStringLiteral("armTargetGroup")); QVBoxLayout *leftTargetsLayout = new QVBoxLayout(leftTargets); leftTargetsLayout->setContentsMargins(0, 4, 6, 0); - QLabel *leftTargetsTitle = new QLabel(QStringLiteral("左臂 · eyou_left_arm"), leftTargets); + QLabel *leftTargetsTitle = new QLabel(QStringLiteral("左臂 · left_ethercat_arm"), leftTargets); leftTargetsTitle->setObjectName(QStringLiteral("eyebrow")); m_leftMotionTable = new QTableWidget(7, 4, leftTargets); m_leftMotionTable->setObjectName(QStringLiteral("leftMotionTable")); leftTargetsLayout->addWidget(leftTargetsTitle); leftTargetsLayout->addWidget(m_leftMotionTable); QWidget *rightTargets = new QWidget(armTargetTables); + rightTargets->setObjectName(QStringLiteral("armTargetGroup")); QVBoxLayout *rightTargetsLayout = new QVBoxLayout(rightTargets); rightTargetsLayout->setContentsMargins(6, 4, 0, 0); - QLabel *rightTargetsTitle = new QLabel(QStringLiteral("右臂 · eyou_right_arm"), rightTargets); + QLabel *rightTargetsTitle = new QLabel(QStringLiteral("右臂 · right_ethercat_arm"), rightTargets); rightTargetsTitle->setObjectName(QStringLiteral("eyebrow")); m_motionTable->setParent(rightTargets); rightTargetsLayout->addWidget(rightTargetsTitle); rightTargetsLayout->addWidget(m_motionTable); armTargetTables->addWidget(leftTargets); armTargetTables->addWidget(rightTargets); - armTargetTables->setSizes({500, 500}); - moveJLayout->addWidget(armTargetTables); + armTargetTables->setSizes({340, 340}); + moveJLayout->addWidget(armTargetTables, 0, 0, 2, 1); + + QFrame *pointPanel = new QFrame(moveJPage); + pointPanel->setObjectName(QStringLiteral("panelCard")); + QVBoxLayout *pointLayout = new QVBoxLayout(pointPanel); + pointLayout->setContentsMargins(12, 10, 12, 10); + pointLayout->setSpacing(8); + QHBoxLayout *pointHeader = new QHBoxLayout; + QLabel *pointTitle = new QLabel(QStringLiteral("关节点位记录与循环"), pointPanel); + pointTitle->setObjectName(QStringLiteral("panelTitle")); + m_jointPointNameEdit = new QLineEdit(pointPanel); + m_jointPointNameEdit->setPlaceholderText(QStringLiteral("动作名称,例如:抓取准备")); + m_jointPointNameEdit->setMinimumWidth(220); + m_recordJointPointButton = new QPushButton(QStringLiteral("记录当前角度"), pointPanel); + pointHeader->addWidget(pointTitle); + pointHeader->addStretch(1); + pointHeader->addWidget(m_jointPointNameEdit); + pointHeader->addWidget(m_recordJointPointButton); + pointLayout->addLayout(pointHeader); + + m_jointPointTable = new QTableWidget(0, 5, pointPanel); + m_jointPointTable->setHorizontalHeaderLabels({ + QStringLiteral("动作名称"), QStringLiteral("机械臂"), QStringLiteral("Device ID"), + QStringLiteral("关节数"), QStringLiteral("关节角度(rad)")}); + m_jointPointTable->setSelectionBehavior(QAbstractItemView::SelectRows); + m_jointPointTable->setSelectionMode(QAbstractItemView::ExtendedSelection); + m_jointPointTable->setEditTriggers(QAbstractItemView::NoEditTriggers); + m_jointPointTable->verticalHeader()->setVisible(false); + m_jointPointTable->verticalHeader()->setDefaultSectionSize(28); + m_jointPointTable->horizontalHeader()->setSectionResizeMode(0, QHeaderView::ResizeToContents); + m_jointPointTable->horizontalHeader()->setSectionResizeMode(1, QHeaderView::ResizeToContents); + m_jointPointTable->horizontalHeader()->setSectionResizeMode(2, QHeaderView::ResizeToContents); + m_jointPointTable->horizontalHeader()->setSectionResizeMode(3, QHeaderView::ResizeToContents); + m_jointPointTable->horizontalHeader()->setSectionResizeMode(4, QHeaderView::Stretch); + m_jointPointTable->setMinimumHeight(132); + pointLayout->addWidget(m_jointPointTable); + m_jointPointDetailLabel = new QLabel(QStringLiteral("选择点位后查看详细关节角度"), pointPanel); + m_jointPointDetailLabel->setObjectName(QStringLiteral("mutedLabel")); + m_jointPointDetailLabel->setWordWrap(true); + m_jointPointDetailLabel->setTextInteractionFlags(Qt::TextSelectableByMouse); + pointLayout->addWidget(m_jointPointDetailLabel); + + QHBoxLayout *pointActions = new QHBoxLayout; + m_runJointPointButton = new QPushButton(QStringLiteral("执行选中点位"), pointPanel); + m_deleteJointPointButton = new QPushButton(QStringLiteral("删除选中"), pointPanel); + QLabel *loopCountLabel = new QLabel(QStringLiteral("循环次数"), pointPanel); + m_jointPointLoopCount = new QSpinBox(pointPanel); + m_jointPointLoopCount->setRange(1, 10000); + m_jointPointLoopCount->setValue(1); + m_startJointPointLoopButton = new QPushButton(QStringLiteral("执行自定义流程"), pointPanel); + m_stopJointPointLoopButton = new QPushButton(QStringLiteral("停止循环"), pointPanel); + m_stopJointPointLoopButton->setEnabled(false); + m_jointPointLoopStatus = new QLabel(QStringLiteral("未运行"), pointPanel); + m_jointPointLoopStatus->setObjectName(QStringLiteral("mutedLabel")); + pointActions->addWidget(m_runJointPointButton); + pointActions->addWidget(m_deleteJointPointButton); + pointActions->addStretch(1); + pointActions->addWidget(m_jointPointLoopStatus); + pointLayout->addLayout(pointActions); + moveJLayout->addWidget(pointPanel, 0, 1); + + connect(m_recordJointPointButton, &QPushButton::clicked, + this, &MainWindow::recordCurrentJointPoint); + connect(m_runJointPointButton, &QPushButton::clicked, + this, &MainWindow::executeSelectedJointPoint); + connect(m_startJointPointLoopButton, &QPushButton::clicked, + this, &MainWindow::startJointPointLoop); + connect(m_stopJointPointLoopButton, &QPushButton::clicked, this, [this]() { + stopJointPointLoop(QStringLiteral("用户停止")); + m_client->requestArmStop(m_armIdEdit->text().trimmed()); + }); + connect(m_deleteJointPointButton, &QPushButton::clicked, this, [this]() { + if (m_jointPointLoopActive) + return; + QList rows; + for (const QModelIndex &index : m_jointPointTable->selectionModel()->selectedRows()) + rows.append(index.row()); + std::sort(rows.begin(), rows.end(), std::greater()); + QStringList removedNames; + for (int row : rows) { + if (row >= 0 && row < m_recordedJointPoints.size()) { + removedNames.append(m_recordedJointPoints.at(row).name); + m_recordedJointPoints.removeAt(row); + } + } + for (int i = m_jointFlowSteps.size() - 1; i >= 0; --i) { + if (removedNames.contains(m_jointFlowSteps.at(i).pointName)) + m_jointFlowSteps.removeAt(i); + } + refreshRecordedJointPoints(); + refreshJointFlowTable(); + QSettings settings; + saveJointPrograms(settings); + }); + connect(m_jointPointTable, &QTableWidget::cellClicked, this, + [this](int row, int) { + if (row < 0 || row >= m_recordedJointPoints.size()) + return; + const RecordedJointPoint &point = m_recordedJointPoints.at(row); + QStringList details; + for (int i = 0; i < point.jointNames.size() && i < point.positions.size(); ++i) { + details.append(QStringLiteral("%1 = %2 rad") + .arg(point.jointNames.at(i)) + .arg(point.positions.at(i), 0, 'f', 6)); + } + m_jointPointDetailLabel->setText( + QStringLiteral("%1 · %2 · %3\n%4") + .arg(point.name, + point.side == QStringLiteral("left") ? QStringLiteral("左臂") + : QStringLiteral("右臂"), + point.deviceId, details.join(QStringLiteral(" ")))); + }); + + QFrame *flowPanel = new QFrame(moveJPage); + flowPanel->setObjectName(QStringLiteral("panelCard")); + QVBoxLayout *flowLayout = new QVBoxLayout(flowPanel); + flowLayout->setContentsMargins(12, 10, 12, 10); + flowLayout->setSpacing(8); + QHBoxLayout *flowBuilder = new QHBoxLayout; + QLabel *flowTitle = new QLabel(QStringLiteral("自定义执行流程"), flowPanel); + flowTitle->setObjectName(QStringLiteral("panelTitle")); + QLabel *intervalLabel = new QLabel(QStringLiteral("完成后等待"), flowPanel); + m_jointPointInterval = new QDoubleSpinBox(flowPanel); + m_jointPointInterval->setRange(0.0, 3600.0); + m_jointPointInterval->setDecimals(1); + m_jointPointInterval->setValue(1.0); + m_jointPointInterval->setSuffix(QStringLiteral(" s")); + m_jointFlowGroupEdit = new QLineEdit(flowPanel); + m_jointFlowGroupEdit->setPlaceholderText(QStringLiteral("循环组名(留空=执行一次)")); + m_jointFlowGroupEdit->setMaximumWidth(190); + m_jointFlowGroupCount = new QSpinBox(flowPanel); + m_jointFlowGroupCount->setRange(1, 10000); + m_jointFlowGroupCount->setValue(1); + QPushButton *addFlowStepButton = new QPushButton(QStringLiteral("添加选中点位到流程"), flowPanel); + flowBuilder->addWidget(flowTitle); + flowBuilder->addStretch(1); + flowBuilder->addWidget(intervalLabel); + flowBuilder->addWidget(m_jointPointInterval); + flowBuilder->addWidget(m_jointFlowGroupEdit); + flowBuilder->addWidget(new QLabel(QStringLiteral("组循环次数"), flowPanel)); + flowBuilder->addWidget(m_jointFlowGroupCount); + flowBuilder->addWidget(addFlowStepButton); + flowLayout->addLayout(flowBuilder); + m_jointFlowTable = new QTableWidget(0, 5, flowPanel); + m_jointFlowTable->setHorizontalHeaderLabels({QStringLiteral("顺序"), QStringLiteral("动作点位"), + QStringLiteral("完成后等待"), QStringLiteral("循环组"), QStringLiteral("组循环次数")}); + m_jointFlowTable->setSelectionBehavior(QAbstractItemView::SelectRows); + m_jointFlowTable->setSelectionMode(QAbstractItemView::SingleSelection); + m_jointFlowTable->setEditTriggers(QAbstractItemView::NoEditTriggers); + m_jointFlowTable->verticalHeader()->setVisible(false); + m_jointFlowTable->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Stretch); + m_jointFlowTable->setMinimumHeight(120); + flowLayout->addWidget(m_jointFlowTable); + QHBoxLayout *flowActions = new QHBoxLayout; + QPushButton *flowUpButton = new QPushButton(QStringLiteral("上移"), flowPanel); + QPushButton *flowDownButton = new QPushButton(QStringLiteral("下移"), flowPanel); + QPushButton *flowDeleteButton = new QPushButton(QStringLiteral("删除步骤"), flowPanel); + flowActions->addWidget(flowUpButton); + flowActions->addWidget(flowDownButton); + flowActions->addWidget(flowDeleteButton); + flowActions->addStretch(1); + flowActions->addWidget(loopCountLabel); + loopCountLabel->setVisible(false); + m_jointPointLoopCount->setVisible(false); + flowActions->addWidget(m_startJointPointLoopButton); + flowActions->addWidget(m_stopJointPointLoopButton); + flowLayout->addLayout(flowActions); + moveJLayout->addWidget(flowPanel, 1, 1); + + connect(addFlowStepButton, &QPushButton::clicked, this, [this]() { + const QModelIndexList rows = m_jointPointTable->selectionModel()->selectedRows(); + if (rows.isEmpty()) + return; + QList sortedRows; + for (const QModelIndex &index : rows) + sortedRows.append(index.row()); + std::sort(sortedRows.begin(), sortedRows.end()); + for (int row : sortedRows) { + if (row < 0 || row >= m_recordedJointPoints.size()) + continue; + JointFlowStep step; + step.pointName = m_recordedJointPoints.at(row).name; + step.waitSeconds = m_jointPointInterval->value(); + step.loopGroup = m_jointFlowGroupEdit->text().trimmed(); + step.loopCount = step.loopGroup.isEmpty() ? 1 : m_jointFlowGroupCount->value(); + m_jointFlowSteps.append(step); + } + refreshJointFlowTable(); + QSettings settings; + saveJointPrograms(settings); + }); + connect(flowDeleteButton, &QPushButton::clicked, this, [this]() { + const int row = m_jointFlowTable->currentRow(); + if (row >= 0 && row < m_jointFlowSteps.size()) + m_jointFlowSteps.removeAt(row); + refreshJointFlowTable(); + QSettings settings; + saveJointPrograms(settings); + }); + const auto moveFlowStep = [this](int delta) { + const int row = m_jointFlowTable->currentRow(); + const int target = row + delta; + if (row < 0 || target < 0 || target >= m_jointFlowSteps.size()) + return; + m_jointFlowSteps.swap(row, target); + refreshJointFlowTable(); + m_jointFlowTable->selectRow(target); + QSettings settings; + saveJointPrograms(settings); + }; + connect(flowUpButton, &QPushButton::clicked, this, [moveFlowStep]() { moveFlowStep(-1); }); + connect(flowDownButton, &QPushButton::clicked, this, [moveFlowStep]() { moveFlowStep(1); }); m_motionCommandTabs->addTab(moveJPage, QStringLiteral("MoveJ 关节角")); QWidget *moveLPage = new QWidget(m_motionCommandTabs); @@ -480,9 +701,10 @@ QWidget *MainWindow::createArmPage() }); connect(ui.zeroButton, &QPushButton::clicked, this, [this]() { const bool leftArm = m_armSideCombo->currentData().toString() == QStringLiteral("left"); - const int offset = leftArm ? 0 : 7; - for (int i = 0; i < 7 && offset + i < m_targetSpins.size(); ++i) - m_targetSpins.at(offset + i)->setValue(0.0); + const QList &spins = leftArm + ? m_leftTargetSpins : m_rightTargetSpins; + for (QDoubleSpinBox *spin : spins) + spin->setValue(0.0); }); connect(m_sendMoveButton, &QPushButton::clicked, this, [this]() { if (m_motionCommandTabs && m_motionCommandTabs->currentIndex() == 1) @@ -528,18 +750,28 @@ QWidget *MainWindow::createArmPage() this, &MainWindow::appendLog); const auto configureMotionTable = [](QTableWidget *table) { - table->setHorizontalHeaderLabels({QStringLiteral("关节"), QStringLiteral("当前"), - QStringLiteral("目标角度"), QStringLiteral("目标弧度")}); + table->setColumnCount(5); + table->setHorizontalHeaderLabels({QStringLiteral("关节"), QStringLiteral("当前角度"), + QStringLiteral("当前弧度"), QStringLiteral("目标角度"), + QStringLiteral("目标弧度")}); table->setAlternatingRowColors(true); table->setSelectionBehavior(QAbstractItemView::SelectRows); table->setSelectionMode(QAbstractItemView::SingleSelection); table->setShowGrid(false); table->verticalHeader()->setVisible(false); - table->verticalHeader()->setDefaultSectionSize(32); - table->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Stretch); - table->horizontalHeader()->setSectionResizeMode(1, QHeaderView::ResizeToContents); - table->horizontalHeader()->setSectionResizeMode(2, QHeaderView::Stretch); - table->horizontalHeader()->setSectionResizeMode(3, QHeaderView::Stretch); + table->verticalHeader()->setDefaultSectionSize(29); + table->horizontalHeader()->setFixedHeight(32); + table->horizontalHeader()->setHighlightSections(false); + for (int column = 0; column < 5; ++column) + table->horizontalHeader()->setSectionResizeMode(column, QHeaderView::Fixed); + table->setColumnWidth(0, 112); + table->setColumnWidth(1, 76); + table->setColumnWidth(2, 92); + table->setColumnWidth(3, 118); + table->setColumnWidth(4, 96); + table->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + table->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); + table->setMinimumHeight(32 + 7 * 29 + 2); }; configureMotionTable(m_leftMotionTable); configureMotionTable(m_motionTable); @@ -554,14 +786,21 @@ QWidget *MainWindow::createArmPage() m_jointTable->horizontalHeader()->setStretchLastSection(true); m_targetSpins.clear(); - const auto populateMotionTable = [this](QTableWidget *table, bool leftArm) { - const QStringList jointNames = armJointNames(leftArm); + m_leftArmJointNames = armJointNames(true); + m_rightArmJointNames = armJointNames(false); + const auto populateMotionTable = [this](QTableWidget *table, bool leftArm, + const QStringList &jointNames) { + QList &sideSpins = leftArm + ? m_leftTargetSpins : m_rightTargetSpins; + sideSpins.clear(); table->clearContents(); table->setRowCount(jointNames.size()); + table->setMinimumHeight(32 + qMin(jointNames.size(), 10) * 29 + 2); for (int row = 0; row < jointNames.size(); ++row) { const QString jointName = jointNames.at(row); table->setItem(row, 0, new QTableWidgetItem(jointName)); table->setItem(row, 1, new QTableWidgetItem(QStringLiteral("--"))); + table->setItem(row, 2, new QTableWidgetItem(QStringLiteral("--"))); QDoubleSpinBox *target = new QDoubleSpinBox(table); target->setRange(-180.0, 180.0); target->setDecimals(3); @@ -570,14 +809,15 @@ QWidget *MainWindow::createArmPage() target->setFrame(false); target->setEnabled(m_connected); m_targetSpins.append(target); - table->setCellWidget(row, 2, target); + sideSpins.append(target); + table->setCellWidget(row, 3, target); QTableWidgetItem *radians = new QTableWidgetItem(QStringLiteral("0.000000")); radians->setFlags(radians->flags() & ~Qt::ItemIsEditable); - table->setItem(row, 3, radians); + table->setItem(row, 4, radians); connect(target, QOverload::of(&QDoubleSpinBox::valueChanged), this, [this, table, row, jointName](double degrees) { - if (row < table->rowCount() && table->item(row, 3)) - table->item(row, 3)->setText( + if (row < table->rowCount() && table->item(row, 4)) + table->item(row, 4)->setText( QString::number(qDegreesToRadians(degrees), 'f', 6)); if (m_teachModeButton->isChecked()) m_urdfViewer->setTeachingJointPosition( @@ -586,8 +826,8 @@ QWidget *MainWindow::createArmPage() } }; - populateMotionTable(m_leftMotionTable, true); - populateMotionTable(m_motionTable, false); + populateMotionTable(m_leftMotionTable, true, m_leftArmJointNames); + populateMotionTable(m_motionTable, false, m_rightArmJointNames); const QStringList allJointNames = armJointNames(true) + armJointNames(false); m_jointTable->clearContents(); @@ -600,7 +840,7 @@ QWidget *MainWindow::createArmPage() const auto refreshActiveArmControls = [this]() { const bool leftArm = m_armSideCombo->currentData().toString() == QStringLiteral("left"); - m_activeArmJointNames = armJointNames(leftArm); + m_activeArmJointNames = leftArm ? m_leftArmJointNames : m_rightArmJointNames; QStringList zeroJointNames; const QStringList urdfNames = m_urdfViewer->jointNames(); for (const QString &name : urdfNames) { @@ -628,8 +868,8 @@ QWidget *MainWindow::createArmPage() if (telemetryRunning) m_client->setArmTelemetry(oldDeviceId, false); const bool leftArm = m_armSideCombo->currentData().toString() == QStringLiteral("left"); - m_armIdEdit->setText(leftArm ? QStringLiteral("eyou_left_arm") - : QStringLiteral("eyou_right_arm")); + m_armIdEdit->setText(leftArm ? QStringLiteral("left_ethercat_arm") + : QStringLiteral("right_ethercat_arm")); m_latestArmPose.clear(); m_poseValue->setText(QStringLiteral("X --\nY --\nZ --\n\nRX --\nRY --\nRZ --")); poseSubtitle->setText(leftArm ? QStringLiteral("PELVIS_S → L_FINGER_TIP") @@ -641,9 +881,40 @@ QWidget *MainWindow::createArmPage() m_client->setArmTelemetry(m_armIdEdit->text().trimmed(), true); }); connect(m_urdfViewer, &UrdfViewerWidget::modelLoaded, this, - [this, refreshActiveArmControls](const QString &, int, int) { + [this, refreshActiveArmControls, populateMotionTable](const QString &, int, int) { + const QStringList urdfNames = m_urdfViewer->jointNames(); + QStringList leftNames; + QStringList rightNames; + for (const QString &name : urdfNames) { + if (isArmJointForSide(name, true)) + leftNames.append(name); + else if (isArmJointForSide(name, false)) + rightNames.append(name); + } + // A single-arm URDF often uses neutral names such as joint1..joint6. + // In that case bind all movable joints to the currently selected arm. + if (leftNames.isEmpty() && rightNames.isEmpty() && !urdfNames.isEmpty()) { + if (m_armSideCombo->currentData().toString() == QStringLiteral("left")) + leftNames = urdfNames; + else + rightNames = urdfNames; + } + m_leftArmJointNames = leftNames; + m_rightArmJointNames = rightNames; + m_targetSpins.clear(); + populateMotionTable(m_leftMotionTable, true, m_leftArmJointNames); + populateMotionTable(m_motionTable, false, m_rightArmJointNames); + + m_jointTable->clearContents(); + m_jointTable->setRowCount(urdfNames.size()); + for (int row = 0; row < urdfNames.size(); ++row) { + m_jointTable->setItem(row, 0, new QTableWidgetItem(urdfNames.at(row))); + for (int column = 1; column < 4; ++column) + m_jointTable->setItem(row, column, + new QTableWidgetItem(QStringLiteral("--"))); + } refreshActiveArmControls(); - m_teachModeButton->setEnabled(!m_urdfViewer->jointNames().isEmpty()); + m_teachModeButton->setEnabled(!urdfNames.isEmpty()); }); connect(m_teachModeButton, &QPushButton::toggled, this, [this](bool enabled) { m_urdfViewer->setTeachingEnabled(enabled); @@ -683,15 +954,16 @@ QWidget *MainWindow::createArmPage() for (int side = 0; side < 2; ++side) { const bool leftArm = side == 0; QTableWidget *table = leftArm ? m_leftMotionTable : m_motionTable; - const QStringList names = armJointNames(leftArm); - const int offset = leftArm ? 0 : 7; - for (int row = 0; row < names.size() && offset + row < m_targetSpins.size(); ++row) { + const QStringList &names = leftArm ? m_leftArmJointNames : m_rightArmJointNames; + const QList &spins = leftArm + ? m_leftTargetSpins : m_rightTargetSpins; + for (int row = 0; row < names.size() && row < spins.size(); ++row) { if (!positions.contains(names.at(row))) continue; const double radians = positions.value(names.at(row)); - const QSignalBlocker blocker(m_targetSpins.at(offset + row)); - m_targetSpins.at(offset + row)->setValue(qRadiansToDegrees(radians)); - table->item(row, 3)->setText(QString::number(radians, 'f', 6)); + const QSignalBlocker blocker(spins.at(row)); + spins.at(row)->setValue(qRadiansToDegrees(radians)); + table->item(row, 4)->setText(QString::number(radians, 'f', 6)); } } }); @@ -705,7 +977,7 @@ QWidget *MainWindow::createArmPage() leftArm ? QStringLiteral("left") : QStringLiteral("right")); if (sideIndex >= 0 && sideIndex != m_armSideCombo->currentIndex()) m_armSideCombo->setCurrentIndex(sideIndex); - const QStringList names = armJointNames(leftArm); + const QStringList &names = leftArm ? m_leftArmJointNames : m_rightArmJointNames; if (m_teachModeButton->isChecked() && row >= 0 && row < names.size()) m_urdfViewer->selectTeachingJoint(names.at(row)); }); diff --git a/src/robotclient.cpp b/src/robotclient.cpp index a27ae31..ac86655 100644 --- a/src/robotclient.cpp +++ b/src/robotclient.cpp @@ -2648,7 +2648,6 @@ private: void pollGrpc() { - ++m_tick; if (m_armPollingEnabled && !m_armDeviceId.isEmpty()) { cmvr::api::JointRequest request; fillHeader(request.mutable_header(), m_armDeviceId); @@ -2675,45 +2674,6 @@ private: emit logMessage(QStringLiteral("WARN"), QStringLiteral("ArmService 连续 10 次遥测请求失败")); } - if (m_tick % 5 == 0) { - cmvr::api::GetPose::Request poseRequest; - fillHeader(poseRequest.mutable_header(), m_armDeviceId); - poseRequest.set_base_link("PELVIS_S"); - poseRequest.set_ee_link( - m_armDeviceId.contains(QStringLiteral("left"), Qt::CaseInsensitive) - ? "L_FINGER_TIP" : "R_FINGER_TIP"); - cmvr::api::GetPose::Response poseResponse; - grpc::ClientContext poseContext; - setDeadline(poseContext, 400); - const grpc::Status poseStatus = m_arm->getPose(&poseContext, poseRequest, &poseResponse); - if (poseStatus.ok() && poseResponse.header().success()) { - const auto &source = poseResponse.pose(); - QVariantMap pose; - pose.insert(QStringLiteral("x"), source.x()); - pose.insert(QStringLiteral("y"), source.y()); - pose.insert(QStringLiteral("z"), source.z()); - pose.insert(QStringLiteral("rx"), source.rx()); - pose.insert(QStringLiteral("ry"), source.ry()); - pose.insert(QStringLiteral("rz"), source.rz()); - emit telemetryReceived(QVariantList(), pose); - } - } - } - - if (m_tick % 10 != 0) - return; - cmvr::api::GetSystemStatusCommand::Request statusRequest; - cmvr::api::GetSystemStatusCommand::Feedback statusResponse; - grpc::ClientContext statusContext; - setDeadline(statusContext, 500); - const grpc::Status rpcStatus = m_system->GetSystemStatus(&statusContext, statusRequest, &statusResponse); - if (rpcStatus.ok() && statusResponse.header().success()) { - const double memory = statusResponse.mem_total_mb() > 0 - ? statusResponse.mem_used_mb() * 100.0 / statusResponse.mem_total_mb() : 0.0; - const double disk = statusResponse.disk_total_gb() > 0 - ? statusResponse.disk_used_gb() * 100.0 / statusResponse.disk_total_gb() : 0.0; - emit systemStatusReceived(statusResponse.cpu_usage(), memory, disk, - statusResponse.device_list_size()); } } diff --git a/src/urdfviewerwidget.cpp b/src/urdfviewerwidget.cpp index c1ec512..346262d 100644 --- a/src/urdfviewerwidget.cpp +++ b/src/urdfviewerwidget.cpp @@ -44,6 +44,7 @@ #include #include #include +#include #include #include #include @@ -406,7 +407,29 @@ void UrdfViewerWidget::setLoading(bool loading, const QString &message) UrdfViewerWidget::~UrdfViewerWidget() { - // Qt3DWindow's aspect engine owns and synchronously tears down the scene. + if (m_demoTimer) + m_demoTimer->stop(); + + // Qt 5.12/MinGW has a reproducible access violation in Qt53DRender.dll + // when an embedded Qt3DWindow is destroyed while its render aspect is + // winding down. Detach this process-lifetime viewer so normal QWidget + // destruction cannot enter the faulty Qt3D teardown path; the OS reclaims + // it immediately when the process exits. + if (m_viewContainer) + m_viewContainer->removeEventFilter(this); + if (m_view) + m_view->removeEventFilter(this); + if (m_viewContainer) { + m_layout->removeWidget(m_viewContainer); + m_viewContainer->hide(); + m_viewContainer->setParent(nullptr); + } + m_viewContainer = nullptr; + m_view = nullptr; + m_camera = nullptr; + m_teachingRayCaster = nullptr; + m_modelRoot = nullptr; + m_rootEntity = nullptr; } void UrdfViewerWidget::showEvent(QShowEvent *event) @@ -624,8 +647,7 @@ bool UrdfViewerWidget::loadUrdf(const QString &filePath) return false; } - setLoading(true, QStringLiteral("正在初始化 Qt3D 场景…")); - ensureSceneInitialized(); + setLoading(true, QStringLiteral("正在解析 URDF 关节数据…")); m_statusLabel->setText(QStringLiteral("正在解析 %1…").arg(fileInfo.fileName())); QFile file(fileInfo.absoluteFilePath()); @@ -647,6 +669,60 @@ bool UrdfViewerWidget::loadUrdf(const QString &filePath) return false; } + { + // Qt 5.12.4/MinGW's Qt3D renderer crashes in its backend entity + // synchronization code while a URDF component tree is being published. + // Parse the complete URDF for control tables without creating a 3D scene. + clearModel(); + m_currentFile = fileInfo.absoluteFilePath(); + m_urdfDirectory = fileInfo.absolutePath(); + m_pathEdit->setText(fileInfo.fileName()); + m_pathEdit->setToolTip(QDir::toNativeSeparators(m_currentFile)); + m_reloadButton->setEnabled(true); + + std::function collectJoints; + collectJoints = [this, &collectJoints](const urdf::LinkConstSharedPtr &link) { + if (!link) + return; + for (const urdf::JointSharedPtr &joint : link->child_joints) { + if (!joint) + continue; + if (joint->type == urdf::Joint::REVOLUTE + || joint->type == urdf::Joint::CONTINUOUS + || joint->type == urdf::Joint::PRISMATIC) { + const QString jointName = QString::fromStdString(joint->name); + m_jointOrder.append(jointName); + m_jointPositions.insert(jointName, + m_latestTelemetryPositions.value(jointName, 0.0)); + } + urdf::LinkConstSharedPtr childLink; + for (const urdf::LinkSharedPtr &candidate : link->child_links) { + if (candidate && candidate->name == joint->child_link_name) { + childLink = candidate; + break; + } + } + collectJoints(childLink); + } + }; + collectJoints(model->getRoot()); + + const int linkCount = static_cast(model->links_.size()); + const int jointCount = static_cast(model->joints_.size()); + m_modelSummary = QStringLiteral("%1 个 Link · %2 个 Joint · 仅关节数据") + .arg(linkCount).arg(jointCount); + m_statusLabel->setText(m_modelSummary); + m_loadingTitle->setText(QStringLiteral("3D 模型预览暂时关闭")); + m_loadingDetail->setText(QStringLiteral("URDF 已解析,关节表和控制功能可正常使用")); + m_loadingProgress->setVisible(false); + m_viewportPlaceholder->show(); + emit logMessage(QStringLiteral("OK"), + QStringLiteral("URDF 已加载(关节数据模式):%1(%2 Link,%3 Joint)") + .arg(fileInfo.fileName()).arg(linkCount).arg(jointCount)); + emit modelLoaded(m_currentFile, linkCount, jointCount); + return true; + } + clearModel(); m_currentFile = fileInfo.absoluteFilePath(); m_urdfDirectory = fileInfo.absolutePath(); @@ -740,20 +816,24 @@ bool UrdfViewerWidget::loadUrdf(const QString &filePath) break; } Qt3DRender::QSceneLoader *loader = new Qt3DRender::QSceneLoader(geometryEntity); + const QPointer guardedLoader(loader); + const QPointer guardedGeometry(geometryEntity); connect(loader, &Qt3DRender::QSceneLoader::statusChanged, this, - [this, loader, source, geometryEntity, teachingJointName] + [this, guardedLoader, source, guardedGeometry, teachingJointName] (Qt3DRender::QSceneLoader::Status status) { + if (!guardedLoader || !guardedGeometry) + return; if (status == Qt3DRender::QSceneLoader::Error) { emit logMessage(QStringLiteral("ERROR"), QStringLiteral("Qt3D 无法加载网格:%1").arg(source)); } else if (status == Qt3DRender::QSceneLoader::Ready && !teachingJointName.isEmpty()) { - const QStringList entityNames = loader->entityNames(); + const QStringList entityNames = guardedLoader->entityNames(); for (const QString &entityName : entityNames) { - Qt3DCore::QEntity *loadedEntity = loader->entity(entityName); + Qt3DCore::QEntity *loadedEntity = guardedLoader->entity(entityName); if (!loadedEntity) continue; - if (loader->component( + if (guardedLoader->component( entityName, Qt3DRender::QSceneLoader::GeometryRendererComponent)) { attachTeachingPicker(loadedEntity, teachingJointName); @@ -761,7 +841,7 @@ bool UrdfViewerWidget::loadUrdf(const QString &filePath) attachTeachingPickersRecursively(loadedEntity, teachingJointName); } - attachTeachingPickersRecursively(geometryEntity, + attachTeachingPickersRecursively(guardedGeometry.data(), teachingJointName); } if (m_view) @@ -1181,18 +1261,16 @@ QString UrdfViewerWidget::resolveMeshPath(const QString &uri) const void UrdfViewerWidget::scheduleViewAll() { + // QCamera::viewEntity() starts a backend bounding-volume query. In Qt + // 5.12 on Windows that query can race the first scene synchronization and + // crash inside Qt53DRender.dll. The viewer already has a deterministic + // camera in resetView(), so only request frames after the scene settles. QTimer::singleShot(120, this, [this]() { - if (m_camera && m_modelRoot) { - m_camera->viewEntity(m_modelRoot); - if (m_view) - m_view->requestUpdate(); - } + if (m_view && m_modelRoot) + m_view->requestUpdate(); }); QTimer::singleShot(700, this, [this]() { - if (m_camera && m_modelRoot) { - m_camera->viewEntity(m_modelRoot); - if (m_view) - m_view->requestUpdate(); - } + if (m_view && m_modelRoot) + m_view->requestUpdate(); }); } diff --git a/styles.qss b/styles.qss index a1717cf..295233f 100644 --- a/styles.qss +++ b/styles.qss @@ -337,9 +337,51 @@ QTabWidget#armMotionCommandTabs QTabBar::tab:selected { font-weight: 600; } QSplitter#armTargetTables::handle { - background: #dfe5ec; - width: 1px; - margin: 26px 5px 4px 5px; + background: transparent; + width: 12px; +} +QWidget#armTargetGroup { + background: transparent; +} +QTableWidget#leftMotionTable, +QTableWidget#motionTable { + background: #ffffff; + alternate-background-color: #f8fafc; + border: 1px solid #dce3ea; + border-radius: 6px; + color: #26323d; +} +QTableWidget#leftMotionTable QHeaderView::section, +QTableWidget#motionTable QHeaderView::section { + background: #eef3f7; + color: #526171; + border: none; + border-bottom: 1px solid #d7e0e8; + padding: 6px 8px; + font-size: 11px; + font-weight: 600; +} +QTableWidget#leftMotionTable::item, +QTableWidget#motionTable::item { + padding: 3px 8px; + border-bottom: 1px solid #edf1f4; +} +QTableWidget#leftMotionTable QDoubleSpinBox, +QTableWidget#motionTable QDoubleSpinBox { + min-height: 24px; + padding: 1px 7px; + background: #f5f8fb; + border: 1px solid transparent; + border-radius: 4px; + color: #1d5fa7; + font-weight: 600; +} +QTableWidget#leftMotionTable QDoubleSpinBox:hover, +QTableWidget#motionTable QDoubleSpinBox:hover, +QTableWidget#leftMotionTable QDoubleSpinBox:focus, +QTableWidget#motionTable QDoubleSpinBox:focus { + background: #ffffff; + border-color: #6aa7ea; } QFrame#urdfToolbar, QFrame#urdfStatusBar {