Compare commits

...

1 Commits

Author SHA1 Message Date
19264915b4 feat: improve arm control workflow and Windows stability 2026-09-23 17:25:01 +08:00
7 changed files with 948 additions and 124 deletions

View File

@ -11,8 +11,94 @@
#include <QTimer> #include <QTimer>
#include <QTextStream> #include <QTextStream>
#ifdef Q_OS_WIN
#include <windows.h>
#include <dbghelp.h>
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<MINIDUMP_TYPE>(
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<DWORD>(length), &written, nullptr);
FlushFileBuffers(summaryFile);
CloseHandle(summaryFile);
}
return EXCEPTION_EXECUTE_HANDLER;
}
} // namespace
#endif
int main(int argc, char *argv[]) int main(int argc, char *argv[])
{ {
#ifdef Q_OS_WIN
SetUnhandledExceptionFilter(writeCrashDump);
#endif
QApplication app(argc, argv); QApplication app(argc, argv);
app.setApplicationName(QStringLiteral("CMVR Control Station")); app.setApplicationName(QStringLiteral("CMVR Control Station"));
app.setOrganizationName(QStringLiteral("CMVR")); app.setOrganizationName(QStringLiteral("CMVR"));

View File

@ -659,6 +659,7 @@ void MainWindow::updateTelemetry(const QVariantList &joints, const QVariantMap &
byName.insert(name, joint); byName.insert(name, joint);
jointPositions.insert(name, joint.value(QStringLiteral("position")).toDouble()); jointPositions.insert(name, joint.value(QStringLiteral("position")).toDouble());
} }
m_latestArmJointPositions = jointPositions;
if (m_urdfViewer) if (m_urdfViewer)
m_urdfViewer->setJointPositions(jointPositions); m_urdfViewer->setJointPositions(jointPositions);
@ -689,10 +690,13 @@ void MainWindow::updateTelemetry(const QVariantList &joints, const QVariantMap &
if (!nameItem) if (!nameItem)
continue; continue;
const QVariantMap joint = byName.value(nameItem->text()); 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( table->item(row, 1)->setText(QStringLiteral("%1°").arg(
qRadiansToDegrees(joint.value(QStringLiteral("position")).toDouble()), qRadiansToDegrees(position),
0, 'f', 2)); 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"), appendLog(success ? QStringLiteral("OK") : QStringLiteral("ERROR"),
QStringLiteral("%1 · %2").arg(command, detail)); 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) if (!success)
m_connectionText->setToolTip(detail); m_connectionText->setToolTip(detail);
} }
@ -1003,13 +1020,260 @@ void MainWindow::sendMoveJ()
QVariantList positions; QVariantList positions;
const bool leftArm = m_armSideCombo const bool leftArm = m_armSideCombo
&& m_armSideCombo->currentData().toString() == QStringLiteral("left"); && m_armSideCombo->currentData().toString() == QStringLiteral("left");
const int offset = leftArm ? 0 : 7; const QList<QDoubleSpinBox *> &spins = leftArm
for (int i = 0; i < 7 && offset + i < m_targetSpins.size(); ++i) ? m_leftTargetSpins : m_rightTargetSpins;
positions.append(qDegreesToRadians(m_targetSpins.at(offset + i)->value())); for (QDoubleSpinBox *spin : spins)
positions.append(qDegreesToRadians(spin->value()));
m_client->requestMoveJ(m_armIdEdit->text().trimmed(), positions, m_client->requestMoveJ(m_armIdEdit->text().trimmed(), positions,
m_velocitySpin->value(), m_accelerationSpin->value()); 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() void MainWindow::sendMoveJPose()
{ {
if (m_moveJPoseSpins.size() != 6 || !m_moveJPoseBaseLinkCombo) if (m_moveJPoseSpins.size() != 6 || !m_moveJPoseBaseLinkCombo)
@ -1050,6 +1314,7 @@ void MainWindow::saveSnapshot()
void MainWindow::loadSettings() void MainWindow::loadSettings()
{ {
QSettings settings; QSettings settings;
loadJointPrograms(settings);
m_hostEdit->setText(settings.value(QStringLiteral("connection/host"), QStringLiteral("192.168.0.28")).toString()); m_hostEdit->setText(settings.value(QStringLiteral("connection/host"), QStringLiteral("192.168.0.28")).toString());
m_hostEdit->setCursorPosition(0); m_hostEdit->setCursorPosition(0);
m_portSpin->setValue(settings.value(QStringLiteral("connection/port"), 50052).toInt()); m_portSpin->setValue(settings.value(QStringLiteral("connection/port"), 50052).toInt());
@ -1063,14 +1328,16 @@ void MainWindow::loadSettings()
m_armSideCombo->setCurrentIndex(sideIndex); m_armSideCombo->setCurrentIndex(sideIndex);
} }
const QString defaultArmDeviceId = armSide == QStringLiteral("left") 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"), QString armDeviceId = settings.value(QStringLiteral("arm/device"),
defaultArmDeviceId).toString().trimmed(); defaultArmDeviceId).toString().trimmed();
if (armDeviceId == QStringLiteral("left_arm")) { if (armDeviceId == QStringLiteral("left_arm") ||
armDeviceId = QStringLiteral("eyou_left_arm"); armDeviceId == QStringLiteral("eyou_left_arm")) {
armDeviceId = QStringLiteral("left_ethercat_arm");
} else if (armDeviceId == QStringLiteral("right_arm") || } else if (armDeviceId == QStringLiteral("right_arm") ||
armDeviceId == QStringLiteral("eyou_arm")) { armDeviceId == QStringLiteral("eyou_arm") ||
armDeviceId = QStringLiteral("eyou_right_arm"); armDeviceId == QStringLiteral("eyou_right_arm")) {
armDeviceId = QStringLiteral("right_ethercat_arm");
} }
m_armIdEdit->setText(armDeviceId); m_armIdEdit->setText(armDeviceId);
m_cameraIdEdit->setText(settings.value(QStringLiteral("camera/device"), QStringLiteral("camera")).toString()); m_cameraIdEdit->setText(settings.value(QStringLiteral("camera/device"), QStringLiteral("camera")).toString());
@ -1130,6 +1397,7 @@ void MainWindow::loadSettings()
void MainWindow::saveSettings() void MainWindow::saveSettings()
{ {
QSettings settings; QSettings settings;
saveJointPrograms(settings);
settings.setValue(QStringLiteral("connection/host"), m_hostEdit->text().trimmed()); settings.setValue(QStringLiteral("connection/host"), m_hostEdit->text().trimmed());
settings.setValue(QStringLiteral("connection/port"), m_portSpin->value()); settings.setValue(QStringLiteral("connection/port"), m_portSpin->value());
settings.setValue(QStringLiteral("connection/demo"), m_demoCheck->isChecked()); settings.setValue(QStringLiteral("connection/demo"), m_demoCheck->isChecked());
@ -1168,6 +1436,72 @@ void MainWindow::saveSettings()
: certificatePath); : 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) void MainWindow::setControlsEnabled(bool enabled)
{ {
for (QPushButton *button : m_serviceButtons) { for (QPushButton *button : m_serviceButtons) {

View File

@ -9,6 +9,7 @@
#include <QStringList> #include <QStringList>
#include <QVariantList> #include <QVariantList>
#include <QVariantMap> #include <QVariantMap>
#include <QVector>
#include "videoframe.h" #include "videoframe.h"
@ -25,6 +26,7 @@ class QLineEdit;
class QProgressBar; class QProgressBar;
class QPushButton; class QPushButton;
class QResizeEvent; class QResizeEvent;
class QSettings;
class QSpinBox; class QSpinBox;
class QStackedWidget; class QStackedWidget;
class QTableWidget; class QTableWidget;
@ -73,6 +75,13 @@ private slots:
void appendQuicLog(const QString &level, const QString &message); void appendQuicLog(const QString &level, const QString &message);
void sendMoveJ(); void sendMoveJ();
void sendMoveJPose(); void sendMoveJPose();
void recordCurrentJointPoint();
void executeSelectedJointPoint();
void startJointPointLoop();
void stopJointPointLoop(const QString &reason = QString());
void advanceJointPointLoop();
void refreshRecordedJointPoints();
void refreshJointFlowTable();
void saveSnapshot(); void saveSnapshot();
private: private:
@ -89,6 +98,8 @@ private:
QWidget *createDiagnosticsPage(); QWidget *createDiagnosticsPage();
void loadSettings(); void loadSettings();
void saveSettings(); void saveSettings();
void loadJointPrograms(QSettings &settings);
void saveJointPrograms(QSettings &settings) const;
void setControlsEnabled(bool enabled); void setControlsEnabled(bool enabled);
void updateWindowChrome(); void updateWindowChrome();
void ensureArmModelLoaded(); void ensureArmModelLoaded();
@ -155,6 +166,10 @@ private:
QTableWidget *m_motionTable = nullptr; QTableWidget *m_motionTable = nullptr;
QTableWidget *m_leftMotionTable = nullptr; QTableWidget *m_leftMotionTable = nullptr;
QList<QDoubleSpinBox *> m_targetSpins; QList<QDoubleSpinBox *> m_targetSpins;
QList<QDoubleSpinBox *> m_leftTargetSpins;
QList<QDoubleSpinBox *> m_rightTargetSpins;
QStringList m_leftArmJointNames;
QStringList m_rightArmJointNames;
QStringList m_activeArmJointNames; QStringList m_activeArmJointNames;
QTabWidget *m_motionCommandTabs = nullptr; QTabWidget *m_motionCommandTabs = nullptr;
QList<QDoubleSpinBox *> m_moveJPoseSpins; QList<QDoubleSpinBox *> m_moveJPoseSpins;
@ -180,6 +195,43 @@ private:
QString m_teachSelectedJoint; QString m_teachSelectedJoint;
QString m_pendingUrdfPath; QString m_pendingUrdfPath;
bool m_armModelLoadScheduled = false; bool m_armModelLoadScheduled = false;
struct RecordedJointPoint {
QString name;
QString side;
QString deviceId;
QStringList jointNames;
QVector<double> positions;
};
struct JointFlowStep {
QString pointName;
double waitSeconds = 1.0;
QString loopGroup;
int loopCount = 1;
};
QList<RecordedJointPoint> m_recordedJointPoints;
QList<JointFlowStep> m_jointFlowSteps;
QHash<QString, double> 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<int> m_jointPointLoopRows;
QVector<double> 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; QLineEdit *m_cameraIdEdit = nullptr;
VideoRenderWidget *m_videoRender = nullptr; VideoRenderWidget *m_videoRender = nullptr;

View File

@ -39,6 +39,7 @@
#include <QPushButton> #include <QPushButton>
#include <QPainter> #include <QPainter>
#include <QRegularExpression> #include <QRegularExpression>
#include <QSettings>
#include <QSignalBlocker> #include <QSignalBlocker>
#include <QSlider> #include <QSlider>
#include <QSpinBox> #include <QSpinBox>
@ -143,11 +144,16 @@ QStringList armJointNames(bool leftArm)
bool isArmJointForSide(const QString &jointName, bool leftArm) bool isArmJointForSide(const QString &jointName, bool leftArm)
{ {
const QString upper = jointName.toUpper(); const QString lower = jointName.toLower();
return leftArm ? (upper.startsWith(QStringLiteral("L_")) const QString sideWord = leftArm ? QStringLiteral("left") : QStringLiteral("right");
|| upper.contains(QStringLiteral("LEFT"))) const QString sidePrefix = leftArm ? QStringLiteral("l_") : QStringLiteral("r_");
: (upper.startsWith(QStringLiteral("R_")) return lower.contains(sideWord + QStringLiteral("_arm"))
|| upper.contains(QStringLiteral("RIGHT"))); || 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, QWidget *withRpcExplorer(QWidget *commonPage, RobotClient *client,
@ -247,19 +253,15 @@ QWidget *MainWindow::createArmPage()
ui.commandPanel->setParent(ui.urdfTab); ui.commandPanel->setParent(ui.urdfTab);
ui.urdfTabLayout->insertWidget(0, ui.commandPanel); ui.urdfTabLayout->insertWidget(0, ui.commandPanel);
QSplitter *modelMotionWorkspace = new QSplitter(Qt::Vertical, ui.urdfTab); // Qt3D preview is disabled in release builds. Keep the viewer alive and
modelMotionWorkspace->setObjectName(QStringLiteral("armModelMotionSplitter")); // hidden so it can still parse the URDF and provide the joint list, while
modelMotionWorkspace->setChildrenCollapsible(false); // giving the complete workspace height to the command/point-flow tables.
modelMotionWorkspace->setHandleWidth(8);
ui.urdfTabLayout->removeWidget(ui.urdfViewer); ui.urdfTabLayout->removeWidget(ui.urdfViewer);
modelMotionWorkspace->addWidget(ui.urdfViewer); ui.urdfViewer->hide();
modelMotionWorkspace->addWidget(ui.targetsPanel); ui.targetsPanel->setMinimumHeight(0);
modelMotionWorkspace->setStretchFactor(0, 5); ui.targetsPanel->setMaximumHeight(QWIDGETSIZE_MAX);
modelMotionWorkspace->setStretchFactor(1, 4); ui.targetsPanel->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding);
modelMotionWorkspace->setSizes({500, 350}); ui.urdfTabLayout->insertWidget(1, ui.targetsPanel, 1);
ui.targetsPanel->setMinimumHeight(360);
ui.targetsPanel->setMaximumHeight(440);
ui.urdfTabLayout->insertWidget(1, modelMotionWorkspace, 1);
ui.urdfTabLayout->setStretch(0, 0); ui.urdfTabLayout->setStretch(0, 0);
ui.urdfTabLayout->setStretch(1, 1); ui.urdfTabLayout->setStretch(1, 1);
@ -309,32 +311,251 @@ QWidget *MainWindow::createArmPage()
m_motionCommandTabs->setObjectName(QStringLiteral("armMotionCommandTabs")); m_motionCommandTabs->setObjectName(QStringLiteral("armMotionCommandTabs"));
m_motionCommandTabs->setDocumentMode(true); m_motionCommandTabs->setDocumentMode(true);
QWidget *moveJPage = new QWidget(m_motionCommandTabs); QWidget *moveJPage = new QWidget(m_motionCommandTabs);
QVBoxLayout *moveJLayout = new QVBoxLayout(moveJPage); QGridLayout *moveJLayout = new QGridLayout(moveJPage);
moveJLayout->setContentsMargins(0, 0, 0, 0); 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->setObjectName(QStringLiteral("armTargetTables"));
armTargetTables->setChildrenCollapsible(false); armTargetTables->setChildrenCollapsible(false);
QWidget *leftTargets = new QWidget(armTargetTables); QWidget *leftTargets = new QWidget(armTargetTables);
leftTargets->setObjectName(QStringLiteral("armTargetGroup"));
QVBoxLayout *leftTargetsLayout = new QVBoxLayout(leftTargets); QVBoxLayout *leftTargetsLayout = new QVBoxLayout(leftTargets);
leftTargetsLayout->setContentsMargins(0, 4, 6, 0); 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")); leftTargetsTitle->setObjectName(QStringLiteral("eyebrow"));
m_leftMotionTable = new QTableWidget(7, 4, leftTargets); m_leftMotionTable = new QTableWidget(7, 4, leftTargets);
m_leftMotionTable->setObjectName(QStringLiteral("leftMotionTable")); m_leftMotionTable->setObjectName(QStringLiteral("leftMotionTable"));
leftTargetsLayout->addWidget(leftTargetsTitle); leftTargetsLayout->addWidget(leftTargetsTitle);
leftTargetsLayout->addWidget(m_leftMotionTable); leftTargetsLayout->addWidget(m_leftMotionTable);
QWidget *rightTargets = new QWidget(armTargetTables); QWidget *rightTargets = new QWidget(armTargetTables);
rightTargets->setObjectName(QStringLiteral("armTargetGroup"));
QVBoxLayout *rightTargetsLayout = new QVBoxLayout(rightTargets); QVBoxLayout *rightTargetsLayout = new QVBoxLayout(rightTargets);
rightTargetsLayout->setContentsMargins(6, 4, 0, 0); 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")); rightTargetsTitle->setObjectName(QStringLiteral("eyebrow"));
m_motionTable->setParent(rightTargets); m_motionTable->setParent(rightTargets);
rightTargetsLayout->addWidget(rightTargetsTitle); rightTargetsLayout->addWidget(rightTargetsTitle);
rightTargetsLayout->addWidget(m_motionTable); rightTargetsLayout->addWidget(m_motionTable);
armTargetTables->addWidget(leftTargets); armTargetTables->addWidget(leftTargets);
armTargetTables->addWidget(rightTargets); armTargetTables->addWidget(rightTargets);
armTargetTables->setSizes({500, 500}); armTargetTables->setSizes({340, 340});
moveJLayout->addWidget(armTargetTables); 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<int> rows;
for (const QModelIndex &index : m_jointPointTable->selectionModel()->selectedRows())
rows.append(index.row());
std::sort(rows.begin(), rows.end(), std::greater<int>());
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<int> 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 关节角")); m_motionCommandTabs->addTab(moveJPage, QStringLiteral("MoveJ 关节角"));
QWidget *moveLPage = new QWidget(m_motionCommandTabs); QWidget *moveLPage = new QWidget(m_motionCommandTabs);
@ -480,9 +701,10 @@ QWidget *MainWindow::createArmPage()
}); });
connect(ui.zeroButton, &QPushButton::clicked, this, [this]() { connect(ui.zeroButton, &QPushButton::clicked, this, [this]() {
const bool leftArm = m_armSideCombo->currentData().toString() == QStringLiteral("left"); const bool leftArm = m_armSideCombo->currentData().toString() == QStringLiteral("left");
const int offset = leftArm ? 0 : 7; const QList<QDoubleSpinBox *> &spins = leftArm
for (int i = 0; i < 7 && offset + i < m_targetSpins.size(); ++i) ? m_leftTargetSpins : m_rightTargetSpins;
m_targetSpins.at(offset + i)->setValue(0.0); for (QDoubleSpinBox *spin : spins)
spin->setValue(0.0);
}); });
connect(m_sendMoveButton, &QPushButton::clicked, this, [this]() { connect(m_sendMoveButton, &QPushButton::clicked, this, [this]() {
if (m_motionCommandTabs && m_motionCommandTabs->currentIndex() == 1) if (m_motionCommandTabs && m_motionCommandTabs->currentIndex() == 1)
@ -528,18 +750,28 @@ QWidget *MainWindow::createArmPage()
this, &MainWindow::appendLog); this, &MainWindow::appendLog);
const auto configureMotionTable = [](QTableWidget *table) { const auto configureMotionTable = [](QTableWidget *table) {
table->setHorizontalHeaderLabels({QStringLiteral("关节"), QStringLiteral("当前"), table->setColumnCount(5);
QStringLiteral("目标角度"), QStringLiteral("目标弧度")}); table->setHorizontalHeaderLabels({QStringLiteral("关节"), QStringLiteral("当前角度"),
QStringLiteral("当前弧度"), QStringLiteral("目标角度"),
QStringLiteral("目标弧度")});
table->setAlternatingRowColors(true); table->setAlternatingRowColors(true);
table->setSelectionBehavior(QAbstractItemView::SelectRows); table->setSelectionBehavior(QAbstractItemView::SelectRows);
table->setSelectionMode(QAbstractItemView::SingleSelection); table->setSelectionMode(QAbstractItemView::SingleSelection);
table->setShowGrid(false); table->setShowGrid(false);
table->verticalHeader()->setVisible(false); table->verticalHeader()->setVisible(false);
table->verticalHeader()->setDefaultSectionSize(32); table->verticalHeader()->setDefaultSectionSize(29);
table->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Stretch); table->horizontalHeader()->setFixedHeight(32);
table->horizontalHeader()->setSectionResizeMode(1, QHeaderView::ResizeToContents); table->horizontalHeader()->setHighlightSections(false);
table->horizontalHeader()->setSectionResizeMode(2, QHeaderView::Stretch); for (int column = 0; column < 5; ++column)
table->horizontalHeader()->setSectionResizeMode(3, QHeaderView::Stretch); 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_leftMotionTable);
configureMotionTable(m_motionTable); configureMotionTable(m_motionTable);
@ -554,14 +786,21 @@ QWidget *MainWindow::createArmPage()
m_jointTable->horizontalHeader()->setStretchLastSection(true); m_jointTable->horizontalHeader()->setStretchLastSection(true);
m_targetSpins.clear(); m_targetSpins.clear();
const auto populateMotionTable = [this](QTableWidget *table, bool leftArm) { m_leftArmJointNames = armJointNames(true);
const QStringList jointNames = armJointNames(leftArm); m_rightArmJointNames = armJointNames(false);
const auto populateMotionTable = [this](QTableWidget *table, bool leftArm,
const QStringList &jointNames) {
QList<QDoubleSpinBox *> &sideSpins = leftArm
? m_leftTargetSpins : m_rightTargetSpins;
sideSpins.clear();
table->clearContents(); table->clearContents();
table->setRowCount(jointNames.size()); table->setRowCount(jointNames.size());
table->setMinimumHeight(32 + qMin(jointNames.size(), 10) * 29 + 2);
for (int row = 0; row < jointNames.size(); ++row) { for (int row = 0; row < jointNames.size(); ++row) {
const QString jointName = jointNames.at(row); const QString jointName = jointNames.at(row);
table->setItem(row, 0, new QTableWidgetItem(jointName)); table->setItem(row, 0, new QTableWidgetItem(jointName));
table->setItem(row, 1, new QTableWidgetItem(QStringLiteral("--"))); table->setItem(row, 1, new QTableWidgetItem(QStringLiteral("--")));
table->setItem(row, 2, new QTableWidgetItem(QStringLiteral("--")));
QDoubleSpinBox *target = new QDoubleSpinBox(table); QDoubleSpinBox *target = new QDoubleSpinBox(table);
target->setRange(-180.0, 180.0); target->setRange(-180.0, 180.0);
target->setDecimals(3); target->setDecimals(3);
@ -570,14 +809,15 @@ QWidget *MainWindow::createArmPage()
target->setFrame(false); target->setFrame(false);
target->setEnabled(m_connected); target->setEnabled(m_connected);
m_targetSpins.append(target); m_targetSpins.append(target);
table->setCellWidget(row, 2, target); sideSpins.append(target);
table->setCellWidget(row, 3, target);
QTableWidgetItem *radians = new QTableWidgetItem(QStringLiteral("0.000000")); QTableWidgetItem *radians = new QTableWidgetItem(QStringLiteral("0.000000"));
radians->setFlags(radians->flags() & ~Qt::ItemIsEditable); radians->setFlags(radians->flags() & ~Qt::ItemIsEditable);
table->setItem(row, 3, radians); table->setItem(row, 4, radians);
connect(target, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, connect(target, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this,
[this, table, row, jointName](double degrees) { [this, table, row, jointName](double degrees) {
if (row < table->rowCount() && table->item(row, 3)) if (row < table->rowCount() && table->item(row, 4))
table->item(row, 3)->setText( table->item(row, 4)->setText(
QString::number(qDegreesToRadians(degrees), 'f', 6)); QString::number(qDegreesToRadians(degrees), 'f', 6));
if (m_teachModeButton->isChecked()) if (m_teachModeButton->isChecked())
m_urdfViewer->setTeachingJointPosition( m_urdfViewer->setTeachingJointPosition(
@ -586,8 +826,8 @@ QWidget *MainWindow::createArmPage()
} }
}; };
populateMotionTable(m_leftMotionTable, true); populateMotionTable(m_leftMotionTable, true, m_leftArmJointNames);
populateMotionTable(m_motionTable, false); populateMotionTable(m_motionTable, false, m_rightArmJointNames);
const QStringList allJointNames = armJointNames(true) + armJointNames(false); const QStringList allJointNames = armJointNames(true) + armJointNames(false);
m_jointTable->clearContents(); m_jointTable->clearContents();
@ -600,7 +840,7 @@ QWidget *MainWindow::createArmPage()
const auto refreshActiveArmControls = [this]() { const auto refreshActiveArmControls = [this]() {
const bool leftArm = m_armSideCombo->currentData().toString() == QStringLiteral("left"); const bool leftArm = m_armSideCombo->currentData().toString() == QStringLiteral("left");
m_activeArmJointNames = armJointNames(leftArm); m_activeArmJointNames = leftArm ? m_leftArmJointNames : m_rightArmJointNames;
QStringList zeroJointNames; QStringList zeroJointNames;
const QStringList urdfNames = m_urdfViewer->jointNames(); const QStringList urdfNames = m_urdfViewer->jointNames();
for (const QString &name : urdfNames) { for (const QString &name : urdfNames) {
@ -628,8 +868,8 @@ QWidget *MainWindow::createArmPage()
if (telemetryRunning) if (telemetryRunning)
m_client->setArmTelemetry(oldDeviceId, false); m_client->setArmTelemetry(oldDeviceId, false);
const bool leftArm = m_armSideCombo->currentData().toString() == QStringLiteral("left"); const bool leftArm = m_armSideCombo->currentData().toString() == QStringLiteral("left");
m_armIdEdit->setText(leftArm ? QStringLiteral("eyou_left_arm") m_armIdEdit->setText(leftArm ? QStringLiteral("left_ethercat_arm")
: QStringLiteral("eyou_right_arm")); : QStringLiteral("right_ethercat_arm"));
m_latestArmPose.clear(); m_latestArmPose.clear();
m_poseValue->setText(QStringLiteral("X --\nY --\nZ --\n\nRX --\nRY --\nRZ --")); m_poseValue->setText(QStringLiteral("X --\nY --\nZ --\n\nRX --\nRY --\nRZ --"));
poseSubtitle->setText(leftArm ? QStringLiteral("PELVIS_S → L_FINGER_TIP") poseSubtitle->setText(leftArm ? QStringLiteral("PELVIS_S → L_FINGER_TIP")
@ -641,9 +881,40 @@ QWidget *MainWindow::createArmPage()
m_client->setArmTelemetry(m_armIdEdit->text().trimmed(), true); m_client->setArmTelemetry(m_armIdEdit->text().trimmed(), true);
}); });
connect(m_urdfViewer, &UrdfViewerWidget::modelLoaded, this, 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(); refreshActiveArmControls();
m_teachModeButton->setEnabled(!m_urdfViewer->jointNames().isEmpty()); m_teachModeButton->setEnabled(!urdfNames.isEmpty());
}); });
connect(m_teachModeButton, &QPushButton::toggled, this, [this](bool enabled) { connect(m_teachModeButton, &QPushButton::toggled, this, [this](bool enabled) {
m_urdfViewer->setTeachingEnabled(enabled); m_urdfViewer->setTeachingEnabled(enabled);
@ -683,15 +954,16 @@ QWidget *MainWindow::createArmPage()
for (int side = 0; side < 2; ++side) { for (int side = 0; side < 2; ++side) {
const bool leftArm = side == 0; const bool leftArm = side == 0;
QTableWidget *table = leftArm ? m_leftMotionTable : m_motionTable; QTableWidget *table = leftArm ? m_leftMotionTable : m_motionTable;
const QStringList names = armJointNames(leftArm); const QStringList &names = leftArm ? m_leftArmJointNames : m_rightArmJointNames;
const int offset = leftArm ? 0 : 7; const QList<QDoubleSpinBox *> &spins = leftArm
for (int row = 0; row < names.size() && offset + row < m_targetSpins.size(); ++row) { ? m_leftTargetSpins : m_rightTargetSpins;
for (int row = 0; row < names.size() && row < spins.size(); ++row) {
if (!positions.contains(names.at(row))) if (!positions.contains(names.at(row)))
continue; continue;
const double radians = positions.value(names.at(row)); const double radians = positions.value(names.at(row));
const QSignalBlocker blocker(m_targetSpins.at(offset + row)); const QSignalBlocker blocker(spins.at(row));
m_targetSpins.at(offset + row)->setValue(qRadiansToDegrees(radians)); spins.at(row)->setValue(qRadiansToDegrees(radians));
table->item(row, 3)->setText(QString::number(radians, 'f', 6)); table->item(row, 4)->setText(QString::number(radians, 'f', 6));
} }
} }
}); });
@ -705,7 +977,7 @@ QWidget *MainWindow::createArmPage()
leftArm ? QStringLiteral("left") : QStringLiteral("right")); leftArm ? QStringLiteral("left") : QStringLiteral("right"));
if (sideIndex >= 0 && sideIndex != m_armSideCombo->currentIndex()) if (sideIndex >= 0 && sideIndex != m_armSideCombo->currentIndex())
m_armSideCombo->setCurrentIndex(sideIndex); 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()) if (m_teachModeButton->isChecked() && row >= 0 && row < names.size())
m_urdfViewer->selectTeachingJoint(names.at(row)); m_urdfViewer->selectTeachingJoint(names.at(row));
}); });

View File

@ -2648,7 +2648,6 @@ private:
void pollGrpc() void pollGrpc()
{ {
++m_tick;
if (m_armPollingEnabled && !m_armDeviceId.isEmpty()) { if (m_armPollingEnabled && !m_armDeviceId.isEmpty()) {
cmvr::api::JointRequest request; cmvr::api::JointRequest request;
fillHeader(request.mutable_header(), m_armDeviceId); fillHeader(request.mutable_header(), m_armDeviceId);
@ -2675,45 +2674,6 @@ private:
emit logMessage(QStringLiteral("WARN"), QStringLiteral("ArmService 连续 10 次遥测请求失败")); 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());
} }
} }

View File

@ -44,6 +44,7 @@
#include <QLineEdit> #include <QLineEdit>
#include <QMouseEvent> #include <QMouseEvent>
#include <QProgressBar> #include <QProgressBar>
#include <QPointer>
#include <QPushButton> #include <QPushButton>
#include <QQuaternion> #include <QQuaternion>
#include <QShowEvent> #include <QShowEvent>
@ -406,7 +407,29 @@ void UrdfViewerWidget::setLoading(bool loading, const QString &message)
UrdfViewerWidget::~UrdfViewerWidget() 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) void UrdfViewerWidget::showEvent(QShowEvent *event)
@ -624,8 +647,7 @@ bool UrdfViewerWidget::loadUrdf(const QString &filePath)
return false; return false;
} }
setLoading(true, QStringLiteral("正在初始化 Qt3D 场景…")); setLoading(true, QStringLiteral("正在解析 URDF 关节数据…"));
ensureSceneInitialized();
m_statusLabel->setText(QStringLiteral("正在解析 %1…").arg(fileInfo.fileName())); m_statusLabel->setText(QStringLiteral("正在解析 %1…").arg(fileInfo.fileName()));
QFile file(fileInfo.absoluteFilePath()); QFile file(fileInfo.absoluteFilePath());
@ -647,6 +669,60 @@ bool UrdfViewerWidget::loadUrdf(const QString &filePath)
return false; 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<void(const urdf::LinkConstSharedPtr &)> 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<int>(model->links_.size());
const int jointCount = static_cast<int>(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(); clearModel();
m_currentFile = fileInfo.absoluteFilePath(); m_currentFile = fileInfo.absoluteFilePath();
m_urdfDirectory = fileInfo.absolutePath(); m_urdfDirectory = fileInfo.absolutePath();
@ -740,20 +816,24 @@ bool UrdfViewerWidget::loadUrdf(const QString &filePath)
break; break;
} }
Qt3DRender::QSceneLoader *loader = new Qt3DRender::QSceneLoader(geometryEntity); Qt3DRender::QSceneLoader *loader = new Qt3DRender::QSceneLoader(geometryEntity);
const QPointer<Qt3DRender::QSceneLoader> guardedLoader(loader);
const QPointer<Qt3DCore::QEntity> guardedGeometry(geometryEntity);
connect(loader, &Qt3DRender::QSceneLoader::statusChanged, this, connect(loader, &Qt3DRender::QSceneLoader::statusChanged, this,
[this, loader, source, geometryEntity, teachingJointName] [this, guardedLoader, source, guardedGeometry, teachingJointName]
(Qt3DRender::QSceneLoader::Status status) { (Qt3DRender::QSceneLoader::Status status) {
if (!guardedLoader || !guardedGeometry)
return;
if (status == Qt3DRender::QSceneLoader::Error) { if (status == Qt3DRender::QSceneLoader::Error) {
emit logMessage(QStringLiteral("ERROR"), emit logMessage(QStringLiteral("ERROR"),
QStringLiteral("Qt3D 无法加载网格:%1").arg(source)); QStringLiteral("Qt3D 无法加载网格:%1").arg(source));
} else if (status == Qt3DRender::QSceneLoader::Ready } else if (status == Qt3DRender::QSceneLoader::Ready
&& !teachingJointName.isEmpty()) { && !teachingJointName.isEmpty()) {
const QStringList entityNames = loader->entityNames(); const QStringList entityNames = guardedLoader->entityNames();
for (const QString &entityName : entityNames) { for (const QString &entityName : entityNames) {
Qt3DCore::QEntity *loadedEntity = loader->entity(entityName); Qt3DCore::QEntity *loadedEntity = guardedLoader->entity(entityName);
if (!loadedEntity) if (!loadedEntity)
continue; continue;
if (loader->component( if (guardedLoader->component(
entityName, entityName,
Qt3DRender::QSceneLoader::GeometryRendererComponent)) { Qt3DRender::QSceneLoader::GeometryRendererComponent)) {
attachTeachingPicker(loadedEntity, teachingJointName); attachTeachingPicker(loadedEntity, teachingJointName);
@ -761,7 +841,7 @@ bool UrdfViewerWidget::loadUrdf(const QString &filePath)
attachTeachingPickersRecursively(loadedEntity, attachTeachingPickersRecursively(loadedEntity,
teachingJointName); teachingJointName);
} }
attachTeachingPickersRecursively(geometryEntity, attachTeachingPickersRecursively(guardedGeometry.data(),
teachingJointName); teachingJointName);
} }
if (m_view) if (m_view)
@ -1181,18 +1261,16 @@ QString UrdfViewerWidget::resolveMeshPath(const QString &uri) const
void UrdfViewerWidget::scheduleViewAll() 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]() { QTimer::singleShot(120, this, [this]() {
if (m_camera && m_modelRoot) { if (m_view && m_modelRoot)
m_camera->viewEntity(m_modelRoot);
if (m_view)
m_view->requestUpdate(); m_view->requestUpdate();
}
}); });
QTimer::singleShot(700, this, [this]() { QTimer::singleShot(700, this, [this]() {
if (m_camera && m_modelRoot) { if (m_view && m_modelRoot)
m_camera->viewEntity(m_modelRoot);
if (m_view)
m_view->requestUpdate(); m_view->requestUpdate();
}
}); });
} }

View File

@ -337,9 +337,51 @@ QTabWidget#armMotionCommandTabs QTabBar::tab:selected {
font-weight: 600; font-weight: 600;
} }
QSplitter#armTargetTables::handle { QSplitter#armTargetTables::handle {
background: #dfe5ec; background: transparent;
width: 1px; width: 12px;
margin: 26px 5px 4px 5px; }
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 { QFrame#urdfToolbar, QFrame#urdfStatusBar {