#include "configeditorwidget.h" #include "robotclient.h" #include "uiicons.h" #include "cmvr/config/device_manager_config/device_manager_config.pb.h" #include "cmvr/config/task_manager_config/task_manager_config.pb.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { constexpr int kPathRole = Qt::UserRole; constexpr int kMessageRole = Qt::UserRole + 1; constexpr int kFieldRole = Qt::UserRole + 2; constexpr int kIndexRole = Qt::UserRole + 3; constexpr int kPreviousValueRole = Qt::UserRole + 4; struct DevicePresentation { QString name; QString icon; QString accent; }; QString scalarValue(const google::protobuf::Message *message, const google::protobuf::FieldDescriptor *field, int index); class DeviceCardFrame final : public QFrame { public: using QFrame::QFrame; std::function activated; protected: void mouseReleaseEvent(QMouseEvent *event) override { QFrame::mouseReleaseEvent(event); if (event->button() == Qt::LeftButton && rect().contains(event->pos()) && activated) activated(); } }; int managerTypeForApiType(int type) { switch (type) { case 0: return 17; case 2: return 13; case 3: return 14; case 5: return 15; case 6: return 12; case 7: return 16; case 8: return 8; case 10: return 9; case 11: return 18; case 12: return 19; default: return 0; } } int apiTypeForManagerType(int type) { switch (type) { case 17: return 0; case 13: return 2; case 14: return 3; case 15: return 5; case 12: return 6; case 16: return 7; case 8: return 8; case 9: return 10; case 18: return 11; case 19: return 12; default: return 20; } } QString defaultMessageTypeForManagerType(int type) { switch (type) { case 17: return QStringLiteral("cmvr.config.AGVRootConfig"); case 12: return QStringLiteral("cmvr.config.ArmRootConfig"); case 13: return QStringLiteral("cmvr.config.CameraRootConfig"); case 14: return QStringLiteral("cmvr.config.DexHandRootConfig"); case 15: return QStringLiteral("cmvr.config.MicroPhoneRootConfig"); case 16: return QStringLiteral("cmvr.config.SpeakerRootConfig"); case 8: return QStringLiteral("cmvr.config.BioHeadRobotRootConfig"); default: return QString(); } } QString typeFolderForManagerType(int type) { switch (type) { case 17: return QStringLiteral("agv"); case 12: return QStringLiteral("arm"); case 13: return QStringLiteral("camera"); case 14: return QStringLiteral("dexhand"); case 15: return QStringLiteral("microphone"); case 16: return QStringLiteral("speaker"); case 8: return QStringLiteral("biohead"); default: return QString(); } } std::unique_ptr createMessage(const QString &messageType, const QString &content, QString *error = nullptr) { const auto *descriptor = google::protobuf::DescriptorPool::generated_pool() ->FindMessageTypeByName(messageType.toStdString()); if (!descriptor) { if (error) *error = QStringLiteral("本机未注册配置消息类型 %1").arg(messageType); return {}; } const auto *prototype = google::protobuf::MessageFactory::generated_factory() ->GetPrototype(descriptor); if (!prototype) { if (error) *error = QStringLiteral("无法创建配置消息 %1").arg(messageType); return {}; } std::unique_ptr message(prototype->New()); if (!content.trimmed().isEmpty() && !google::protobuf::TextFormat::ParseFromString(content.toStdString(), message.get())) { if (error) *error = QStringLiteral("配置内容与 %1 不兼容").arg(messageType); return {}; } return message; } google::protobuf::Message *deviceEntry(google::protobuf::Message *root, const QString &deviceId, bool create) { if (!root) return nullptr; const auto *rootDescriptor = root->GetDescriptor(); const auto *rootReflection = root->GetReflection(); const google::protobuf::FieldDescriptor *rootField = nullptr; for (int index = 0; index < rootDescriptor->field_count(); ++index) { if (rootDescriptor->field(index)->cpp_type() == google::protobuf::FieldDescriptor::CPPTYPE_MESSAGE) { rootField = rootDescriptor->field(index); break; } } if (!rootField) return nullptr; auto *container = rootReflection->MutableMessage(root, rootField); const auto *containerDescriptor = container->GetDescriptor(); const auto *containerReflection = container->GetReflection(); const google::protobuf::FieldDescriptor *listField = nullptr; for (int index = 0; index < containerDescriptor->field_count(); ++index) { const auto *field = containerDescriptor->field(index); if (field->is_repeated() && field->cpp_type() == google::protobuf::FieldDescriptor::CPPTYPE_MESSAGE) { listField = field; break; } } if (!listField) return container; const int count = containerReflection->FieldSize(*container, listField); for (int index = 0; index < count; ++index) { auto *entry = containerReflection->MutableRepeatedMessage(container, listField, index); const auto *idField = entry->GetDescriptor()->FindFieldByName("id"); if (idField && QString::fromStdString(entry->GetReflection()->GetString(*entry, idField)) == deviceId) return entry; } return create ? containerReflection->AddMessage(container, listField) : nullptr; } void removeDeviceEntries(google::protobuf::Message *root, const QSet &deviceIds) { if (!root || deviceIds.isEmpty()) return; const auto *rootField = root->GetDescriptor()->field_count() > 0 ? root->GetDescriptor()->field(0) : nullptr; if (!rootField || rootField->cpp_type() != google::protobuf::FieldDescriptor::CPPTYPE_MESSAGE) return; auto *container = root->GetReflection()->MutableMessage(root, rootField); const google::protobuf::FieldDescriptor *listField = nullptr; for (int index = 0; index < container->GetDescriptor()->field_count(); ++index) { const auto *field = container->GetDescriptor()->field(index); if (field->is_repeated() && field->cpp_type() == google::protobuf::FieldDescriptor::CPPTYPE_MESSAGE) { listField = field; break; } } if (!listField) return; const auto *reflection = container->GetReflection(); for (int index = reflection->FieldSize(*container, listField) - 1; index >= 0; --index) { const auto &entry = reflection->GetRepeatedMessage(*container, listField, index); const auto *idField = entry.GetDescriptor()->FindFieldByName("id"); if (!idField) continue; const QString id = QString::fromStdString(entry.GetReflection()->GetString(entry, idField)); if (!deviceIds.contains(id)) continue; const int last = reflection->FieldSize(*container, listField) - 1; if (index != last) reflection->SwapElements(container, listField, index, last); reflection->RemoveLast(container, listField); } } google::protobuf::Message *selectBackend(google::protobuf::Message *entry, const QString &backendName, int brandNumber) { if (!entry) return nullptr; const auto *oneof = entry->GetDescriptor()->FindOneofByName("backend"); if (!oneof) return entry; const auto *field = entry->GetDescriptor()->FindFieldByName(backendName.toStdString()); if (!field || field->containing_oneof() != oneof) return nullptr; auto *backend = entry->GetReflection()->MutableMessage(entry, field); if (brandNumber >= 0) { const auto *brandField = backend->GetDescriptor()->FindFieldByName("brand"); if (brandField && brandField->cpp_type() == google::protobuf::FieldDescriptor::CPPTYPE_ENUM) { if (const auto *brand = brandField->enum_type()->FindValueByNumber(brandNumber)) backend->GetReflection()->SetEnum(backend, brandField, brand); } } return backend; } void appendParameterRows(google::protobuf::Message *message, const QString &prefix, QVariantList *rows) { if (!message || !rows) return; const auto *descriptor = message->GetDescriptor(); const auto *reflection = message->GetReflection(); using Field = google::protobuf::FieldDescriptor; for (int fieldIndex = 0; fieldIndex < descriptor->field_count(); ++fieldIndex) { const auto *field = descriptor->field(fieldIndex); if (field->name() == "id") continue; if (field->containing_oneof()) { const auto *active = reflection->GetOneofFieldDescriptor( *message, field->containing_oneof()); if (active != field) continue; } const QString fieldName = QString::fromStdString(field->name()); const QString path = prefix.isEmpty() ? fieldName : prefix + QLatin1Char('.') + fieldName; if (field->cpp_type() == Field::CPPTYPE_MESSAGE) { if (field->is_repeated()) { const int count = reflection->FieldSize(*message, field); for (int index = 0; index < count; ++index) appendParameterRows(reflection->MutableRepeatedMessage(message, field, index), path + QStringLiteral("[%1]").arg(index), rows); } else { if (!reflection->HasField(*message, field)) continue; appendParameterRows(reflection->MutableMessage(message, field), path, rows); } continue; } const int count = field->is_repeated() ? reflection->FieldSize(*message, field) : 1; const int rowCount = std::max(count, 1); for (int index = 0; index < rowCount; ++index) { QVariantMap row; row.insert(QStringLiteral("path"), field->is_repeated() ? path + QStringLiteral("[%1]").arg(index) : path); row.insert(QStringLiteral("label"), row.value(QStringLiteral("path"), path)); row.insert(QStringLiteral("value"), count == 0 ? QString() : scalarValue(message, field, field->is_repeated() ? index : -1)); row.insert(QStringLiteral("type"), static_cast(field->cpp_type())); row.insert(QStringLiteral("secret"), fieldName.contains(QStringLiteral("password"), Qt::CaseInsensitive)); if (field->cpp_type() == Field::CPPTYPE_ENUM) { QVariantList values; for (int enumIndex = 0; enumIndex < field->enum_type()->value_count(); ++enumIndex) values.append(QString::fromStdString(field->enum_type()->value(enumIndex)->name())); row.insert(QStringLiteral("options"), values); } rows->append(row); } } } bool setScalarField(google::protobuf::Message *message, const google::protobuf::FieldDescriptor *field, int repeatedIndex, const QString &text) { if (!message || !field) return false; auto *reflection = message->GetReflection(); const bool repeated = field->is_repeated(); while (repeated && reflection->FieldSize(*message, field) <= repeatedIndex) { using Field = google::protobuf::FieldDescriptor; switch (field->cpp_type()) { case Field::CPPTYPE_BOOL: reflection->AddBool(message, field, false); break; case Field::CPPTYPE_INT32: reflection->AddInt32(message, field, 0); break; case Field::CPPTYPE_INT64: reflection->AddInt64(message, field, 0); break; case Field::CPPTYPE_UINT32: reflection->AddUInt32(message, field, 0); break; case Field::CPPTYPE_UINT64: reflection->AddUInt64(message, field, 0); break; case Field::CPPTYPE_FLOAT: reflection->AddFloat(message, field, 0); break; case Field::CPPTYPE_DOUBLE: reflection->AddDouble(message, field, 0); break; case Field::CPPTYPE_STRING: reflection->AddString(message, field, std::string()); break; case Field::CPPTYPE_ENUM: reflection->AddEnum(message, field, field->enum_type()->value(0)); break; case Field::CPPTYPE_MESSAGE: return false; } } bool ok = true; using Field = google::protobuf::FieldDescriptor; switch (field->cpp_type()) { case Field::CPPTYPE_BOOL: { const bool value = text.compare(QStringLiteral("true"), Qt::CaseInsensitive) == 0 || text == QStringLiteral("1"); repeated ? reflection->SetRepeatedBool(message, field, repeatedIndex, value) : reflection->SetBool(message, field, value); break; } case Field::CPPTYPE_INT32: { const int value = text.toInt(&ok); if (ok) repeated ? reflection->SetRepeatedInt32(message, field, repeatedIndex, value) : reflection->SetInt32(message, field, value); break; } case Field::CPPTYPE_INT64: { const qlonglong value = text.toLongLong(&ok); if (ok) repeated ? reflection->SetRepeatedInt64(message, field, repeatedIndex, value) : reflection->SetInt64(message, field, value); break; } case Field::CPPTYPE_UINT32: { const uint value = text.toUInt(&ok); if (ok) repeated ? reflection->SetRepeatedUInt32(message, field, repeatedIndex, value) : reflection->SetUInt32(message, field, value); break; } case Field::CPPTYPE_UINT64: { const qulonglong value = text.toULongLong(&ok); if (ok) repeated ? reflection->SetRepeatedUInt64(message, field, repeatedIndex, value) : reflection->SetUInt64(message, field, value); break; } case Field::CPPTYPE_FLOAT: { const float value = text.toFloat(&ok); if (ok) repeated ? reflection->SetRepeatedFloat(message, field, repeatedIndex, value) : reflection->SetFloat(message, field, value); break; } case Field::CPPTYPE_DOUBLE: { const double value = text.toDouble(&ok); if (ok) repeated ? reflection->SetRepeatedDouble(message, field, repeatedIndex, value) : reflection->SetDouble(message, field, value); break; } case Field::CPPTYPE_STRING: repeated ? reflection->SetRepeatedString(message, field, repeatedIndex, text.toStdString()) : reflection->SetString(message, field, text.toStdString()); break; case Field::CPPTYPE_ENUM: { const auto *value = field->enum_type()->FindValueByName(text.toStdString()); ok = value != nullptr; if (ok) repeated ? reflection->SetRepeatedEnum(message, field, repeatedIndex, value) : reflection->SetEnum(message, field, value); break; } case Field::CPPTYPE_MESSAGE: return false; } return ok; } bool setParameterValue(google::protobuf::Message *message, const QString &path, const QString &value) { if (!message || path.isEmpty()) return false; const QString head = path.section(QLatin1Char('.'), 0, 0); const QString tail = path.section(QLatin1Char('.'), 1); static const QRegularExpression indexedPattern( QStringLiteral("^([A-Za-z_][A-Za-z0-9_]*)(?:\\[(\\d+)\\])?$")); const auto match = indexedPattern.match(head); if (!match.hasMatch()) return false; const QString name = match.captured(1); const int index = match.captured(2).isEmpty() ? -1 : match.captured(2).toInt(); const auto *field = message->GetDescriptor()->FindFieldByName(name.toStdString()); if (!field) return false; if (tail.isEmpty()) return setScalarField(message, field, index, value); if (field->cpp_type() != google::protobuf::FieldDescriptor::CPPTYPE_MESSAGE) return false; auto *reflection = message->GetReflection(); google::protobuf::Message *child = nullptr; if (field->is_repeated()) { if (index < 0 || index >= reflection->FieldSize(*message, field)) return false; child = reflection->MutableRepeatedMessage(message, field, index); } else { child = reflection->MutableMessage(message, field); } return setParameterValue(child, tail, value); } QString printMessage(const google::protobuf::Message &message) { std::string output; google::protobuf::TextFormat::Printer printer; printer.SetUseUtf8StringEscaping(true); if (!printer.PrintToString(message, &output)) return QString(); return QString::fromStdString(output); } DevicePresentation presentationForDeviceType(int type) { switch (type) { case 0: return {QStringLiteral("AGV"), QStringLiteral(":/icons/move.svg"), QStringLiteral("#7c5ce7")}; case 1: return {QStringLiteral("电池"), QStringLiteral(":/icons/activity.svg"), QStringLiteral("#d98b21")}; case 2: return {QStringLiteral("相机"), QStringLiteral(":/icons/camera.svg"), QStringLiteral("#2479d8")}; case 3: return {QStringLiteral("灵巧手"), QStringLiteral(":/icons/hand.svg"), QStringLiteral("#a554c7")}; case 4: return {QStringLiteral("夹爪"), QStringLiteral(":/icons/hand.svg"), QStringLiteral("#bd5f8a")}; case 5: return {QStringLiteral("麦克风"), QStringLiteral(":/icons/mic.svg"), QStringLiteral("#168aa2")}; case 6: return {QStringLiteral("机械臂"), QStringLiteral(":/icons/bot.svg"), QStringLiteral("#e26c2d")}; case 7: return {QStringLiteral("扬声器"), QStringLiteral(":/icons/volume-2.svg"), QStringLiteral("#1c9a68")}; case 8: return {QStringLiteral("仿生头"), QStringLiteral(":/icons/scan-face.svg"), QStringLiteral("#d05e8d")}; case 9: return {QStringLiteral("电机"), QStringLiteral(":/icons/activity.svg"), QStringLiteral("#b36b26")}; case 10: return {QStringLiteral("电机系统"), QStringLiteral(":/icons/activity.svg"), QStringLiteral("#a55b24")}; case 11: return {QStringLiteral("MuJoCo 世界"), QStringLiteral(":/icons/braces.svg"), QStringLiteral("#596f8f")}; case 12: return {QStringLiteral("MuJoCo 查看器"), QStringLiteral(":/icons/braces.svg"), QStringLiteral("#61799a")}; case 13: return {QStringLiteral("CAN 总线"), QStringLiteral(":/icons/activity.svg"), QStringLiteral("#596b78")}; default: return {QStringLiteral("未知设备"), QStringLiteral(":/icons/braces.svg"), QStringLiteral("#6f7b87")}; } } DevicePresentation presentationForManagerType(int managerType, int apiType) { if (managerType == 8) return {QStringLiteral("仿生头"), QStringLiteral(":/icons/scan-face.svg"), QStringLiteral("#d05e8d")}; return presentationForDeviceType(apiType); } QTableWidgetItem *readOnlyItem(const QString &text) { auto *item = new QTableWidgetItem(text); item->setFlags(item->flags() & ~Qt::ItemIsEditable); return item; } quintptr pointerValue(const QVariant &value) { return static_cast(value.toULongLong()); } QString fieldTypeName(const google::protobuf::FieldDescriptor *field) { using Field = google::protobuf::FieldDescriptor; if (field->is_repeated()) { if (field->cpp_type() == Field::CPPTYPE_MESSAGE) return QStringLiteral("message[]"); return QString::fromLatin1(field->type_name()) + QStringLiteral("[]"); } return QString::fromLatin1(field->type_name()); } QString enumValueName(const google::protobuf::EnumValueDescriptor *value) { return value ? QString::fromStdString(value->name()) : QString(); } QString scalarValue(const google::protobuf::Message *message, const google::protobuf::FieldDescriptor *field, int index) { const auto *reflection = message->GetReflection(); const bool repeated = field->is_repeated(); using Field = google::protobuf::FieldDescriptor; switch (field->cpp_type()) { case Field::CPPTYPE_BOOL: return (repeated ? reflection->GetRepeatedBool(*message, field, index) : reflection->GetBool(*message, field)) ? QStringLiteral("true") : QStringLiteral("false"); case Field::CPPTYPE_INT32: return QString::number(repeated ? reflection->GetRepeatedInt32(*message, field, index) : reflection->GetInt32(*message, field)); case Field::CPPTYPE_INT64: return QString::number(static_cast( repeated ? reflection->GetRepeatedInt64(*message, field, index) : reflection->GetInt64(*message, field))); case Field::CPPTYPE_UINT32: return QString::number(repeated ? reflection->GetRepeatedUInt32(*message, field, index) : reflection->GetUInt32(*message, field)); case Field::CPPTYPE_UINT64: return QString::number(static_cast( repeated ? reflection->GetRepeatedUInt64(*message, field, index) : reflection->GetUInt64(*message, field))); case Field::CPPTYPE_FLOAT: return QString::number(repeated ? reflection->GetRepeatedFloat(*message, field, index) : reflection->GetFloat(*message, field), 'g', 9); case Field::CPPTYPE_DOUBLE: return QString::number(repeated ? reflection->GetRepeatedDouble(*message, field, index) : reflection->GetDouble(*message, field), 'g', 17); case Field::CPPTYPE_STRING: return QString::fromUtf8((repeated ? reflection->GetRepeatedString(*message, field, index) : reflection->GetString(*message, field)).data()); case Field::CPPTYPE_ENUM: { const auto *value = repeated ? reflection->GetRepeatedEnum(*message, field, index) : reflection->GetEnum(*message, field); return enumValueName(value); } case Field::CPPTYPE_MESSAGE: break; } return QString(); } void setItemMetadata(QTreeWidgetItem *item, google::protobuf::Message *message, const google::protobuf::FieldDescriptor *field, int index, const QString &value) { item->setData(1, kMessageRole, QVariant::fromValue(reinterpret_cast(message))); item->setData(1, kFieldRole, QVariant::fromValue(reinterpret_cast(field))); item->setData(1, kIndexRole, index); item->setData(1, kPreviousValueRole, value); } } ConfigEditorWidget::~ConfigEditorWidget() = default; ConfigEditorWidget::ConfigEditorWidget(RobotClient *client, QWidget *parent) : QWidget(parent), m_client(client) { auto *layout = new QVBoxLayout(this); layout->setContentsMargins(0, 0, 0, 0); layout->setSpacing(0); auto *pageTabs = new QTabWidget(this); pageTabs->setObjectName(QStringLiteral("deviceManagementTabs")); pageTabs->setDocumentMode(true); auto *overviewScroll = new QScrollArea(pageTabs); overviewScroll->setObjectName(QStringLiteral("deviceOverviewScroll")); overviewScroll->setWidgetResizable(true); overviewScroll->setFrameShape(QFrame::NoFrame); auto *overviewPage = new QWidget(overviewScroll); overviewPage->setObjectName(QStringLiteral("deviceOverviewPage")); auto *overviewLayout = new QVBoxLayout(overviewPage); overviewLayout->setContentsMargins(24, 18, 24, 24); overviewLayout->setSpacing(14); auto *hero = new QFrame(overviewPage); hero->setObjectName(QStringLiteral("deviceOverviewHero")); auto *heroLayout = new QHBoxLayout(hero); heroLayout->setContentsMargins(22, 18, 22, 18); heroLayout->setSpacing(18); auto *heroTextLayout = new QVBoxLayout; heroTextLayout->setSpacing(5); auto *heroEyebrow = new QLabel(QStringLiteral("gRPC · SYSTEM SERVICE"), hero); heroEyebrow->setObjectName(QStringLiteral("deviceHeroEyebrow")); auto *heroTitle = new QLabel(QStringLiteral("边缘端设备中心"), hero); heroTitle->setObjectName(QStringLiteral("deviceHeroTitle")); auto *heroDescription = new QLabel( QStringLiteral("自动发现 cmvr-es 注册的在线设备,并在同一入口管理远端配置。"), hero); heroDescription->setObjectName(QStringLiteral("deviceHeroDescription")); heroDescription->setWordWrap(true); m_edgeSystemLabel = new QLabel(QStringLiteral("等待获取边缘端系统信息"), hero); m_edgeSystemLabel->setObjectName(QStringLiteral("deviceSystemIdentity")); heroTextLayout->addWidget(heroEyebrow); heroTextLayout->addWidget(heroTitle); heroTextLayout->addWidget(heroDescription); heroTextLayout->addWidget(m_edgeSystemLabel); heroLayout->addLayout(heroTextLayout, 1); auto *heroActions = new QVBoxLayout; heroActions->setSpacing(10); heroActions->setAlignment(Qt::AlignRight | Qt::AlignVCenter); m_edgeConnectionChip = new QLabel(QStringLiteral("● 边缘端未连接"), hero); m_edgeConnectionChip->setObjectName(QStringLiteral("edgeConnectionChip")); m_edgeConnectionChip->setProperty("connected", false); m_edgeConnectionChip->setAlignment(Qt::AlignCenter); m_deviceRefreshButton = new QPushButton(QStringLiteral("刷新设备"), hero); m_deviceRefreshButton->setObjectName(QStringLiteral("deviceRefreshButton")); m_deviceRefreshButton->setIcon(UiIcons::icon(QStringLiteral(":/icons/rotate-ccw.svg"))); m_deviceRefreshButton->setEnabled(false); heroActions->addWidget(m_edgeConnectionChip); heroActions->addWidget(m_deviceRefreshButton); heroLayout->addLayout(heroActions); overviewLayout->addWidget(hero); auto *metricsLayout = new QHBoxLayout; metricsLayout->setSpacing(12); const auto addMetricCard = [metricsLayout, overviewPage](const QString &label, const QString &value, const QString ¬e, const QString &accent, QLabel **valueLabel) { auto *card = new QFrame(overviewPage); card->setObjectName(QStringLiteral("deviceMetricCard")); card->setProperty("accent", accent); auto *cardLayout = new QVBoxLayout(card); cardLayout->setContentsMargins(16, 13, 16, 13); cardLayout->setSpacing(3); auto *labelWidget = new QLabel(label, card); labelWidget->setObjectName(QStringLiteral("deviceMetricLabel")); auto *valueWidget = new QLabel(value, card); valueWidget->setObjectName(QStringLiteral("deviceMetricValue")); auto *noteWidget = new QLabel(note, card); noteWidget->setObjectName(QStringLiteral("deviceMetricNote")); cardLayout->addWidget(labelWidget); cardLayout->addWidget(valueWidget); cardLayout->addWidget(noteWidget); metricsLayout->addWidget(card, 1); *valueLabel = valueWidget; }; addMetricCard(QStringLiteral("在线设备"), QStringLiteral("--"), QStringLiteral("GetSystemStatus 实时返回"), QStringLiteral("green"), &m_onlineDeviceValue); addMetricCard(QStringLiteral("设备类型"), QStringLiteral("--"), QStringLiteral("按 DeviceType 自动归类"), QStringLiteral("blue"), &m_deviceTypeValue); addMetricCard(QStringLiteral("最近同步"), QStringLiteral("--:--:--"), QStringLiteral("在线状态每秒自动更新"), QStringLiteral("orange"), &m_lastSyncValue); overviewLayout->addLayout(metricsLayout); auto *deviceHeader = new QHBoxLayout; deviceHeader->setContentsMargins(2, 4, 2, 0); auto *deviceHeaderText = new QVBoxLayout; deviceHeaderText->setSpacing(2); m_deviceSectionTitle = new QLabel(QStringLiteral("在线设备"), overviewPage); m_deviceSectionTitle->setObjectName(QStringLiteral("deviceSectionTitle")); m_deviceSectionHint = new QLabel( QStringLiteral("设备 ID 与类型由边缘端 DeviceManager 注册信息生成"), overviewPage); m_deviceSectionHint->setObjectName(QStringLiteral("deviceSectionHint")); deviceHeaderText->addWidget(m_deviceSectionTitle); deviceHeaderText->addWidget(m_deviceSectionHint); deviceHeader->addLayout(deviceHeaderText, 1); m_deviceEditButton = new QPushButton(QStringLiteral("修改"), overviewPage); m_deviceEditButton->setObjectName(QStringLiteral("deviceEditButton")); m_deviceEditButton->setIcon(UiIcons::icon(QStringLiteral(":/icons/edit-3.svg"))); m_deviceEditButton->setEnabled(false); deviceHeader->addWidget(m_deviceEditButton); m_deviceSearchEdit = new QLineEdit(overviewPage); m_deviceSearchEdit->setObjectName(QStringLiteral("deviceSearchEdit")); m_deviceSearchEdit->setPlaceholderText(QStringLiteral("搜索设备 ID 或类型")); m_deviceSearchEdit->setClearButtonEnabled(true); m_deviceSearchEdit->setMinimumWidth(240); deviceHeader->addWidget(m_deviceSearchEdit); overviewLayout->addLayout(deviceHeader); m_deviceGridHost = new QWidget(overviewPage); m_deviceGridHost->setObjectName(QStringLiteral("deviceGridHost")); m_deviceGrid = new QGridLayout(m_deviceGridHost); m_deviceGrid->setContentsMargins(0, 0, 0, 0); m_deviceGrid->setHorizontalSpacing(12); m_deviceGrid->setVerticalSpacing(12); overviewLayout->addWidget(m_deviceGridHost); overviewLayout->addStretch(1); overviewScroll->setWidget(overviewPage); pageTabs->addTab(overviewScroll, QStringLiteral("设备总览")); auto *configPage = new QWidget(pageTabs); auto *configLayout = new QVBoxLayout(configPage); configLayout->setContentsMargins(24, 18, 24, 18); configLayout->setSpacing(12); auto *connectionBar = new QWidget(configPage); connectionBar->setObjectName(QStringLiteral("panel")); auto *connectionLayout = new QHBoxLayout(connectionBar); connectionLayout->setContentsMargins(16, 12, 16, 12); connectionLayout->setSpacing(8); auto *hostLabel = new QLabel(QStringLiteral("配置服务"), connectionBar); hostLabel->setObjectName(QStringLiteral("sectionTitle")); connectionLayout->addWidget(hostLabel); m_hostEdit = new QLineEdit(connectionBar); m_hostEdit->setPlaceholderText(QStringLiteral("设备地址")); m_hostEdit->setMinimumWidth(180); m_portSpin = new QSpinBox(connectionBar); m_portSpin->setRange(1, 65535); m_portSpin->setButtonSymbols(QAbstractSpinBox::NoButtons); m_portSpin->setFixedWidth(76); QSettings settings; m_hostEdit->setText(settings.value(QStringLiteral("configuration/host"), QStringLiteral("192.168.28.248")).toString()); const int savedConfigurationPort = settings.value( QStringLiteral("configuration/port"), 50053).toInt(); m_portSpin->setValue(savedConfigurationPort == 50052 ? 50053 : savedConfigurationPort); connectionLayout->addWidget(m_hostEdit, 1); connectionLayout->addWidget(m_portSpin); m_connectButton = new QPushButton(QStringLiteral("连接"), connectionBar); m_connectButton->setObjectName(QStringLiteral("primaryButton")); m_refreshButton = new QPushButton(QStringLiteral("刷新"), connectionBar); m_refreshButton->setEnabled(false); connectionLayout->addWidget(m_connectButton); connectionLayout->addWidget(m_refreshButton); configLayout->addWidget(connectionBar); auto *splitter = new QSplitter(Qt::Horizontal, configPage); splitter->setChildrenCollapsible(false); m_fileList = new QListWidget(splitter); m_fileList->setMinimumWidth(260); m_fileList->setMaximumWidth(380); m_fileList->setAlternatingRowColors(true); auto *editorPanel = new QWidget(splitter); auto *editorLayout = new QVBoxLayout(editorPanel); editorLayout->setContentsMargins(12, 0, 0, 0); editorLayout->setSpacing(8); m_rootLabel = new QLabel(QStringLiteral("远端根目录:--"), editorPanel); m_rootLabel->setObjectName(QStringLiteral("mutedLabel")); m_rootLabel->setTextInteractionFlags(Qt::TextSelectableByMouse); m_pathLabel = new QLabel(QStringLiteral("未选择配置文件"), editorPanel); m_pathLabel->setObjectName(QStringLiteral("sectionTitle")); m_pathLabel->setTextInteractionFlags(Qt::TextSelectableByMouse); m_typeLabel = new QLabel(QStringLiteral("--"), editorPanel); m_typeLabel->setObjectName(QStringLiteral("mutedLabel")); editorLayout->addWidget(m_rootLabel); editorLayout->addWidget(m_pathLabel); editorLayout->addWidget(m_typeLabel); m_editorTabs = new QTabWidget(editorPanel); m_textEditor = new QPlainTextEdit(m_editorTabs); m_textEditor->setLineWrapMode(QPlainTextEdit::NoWrap); m_textEditor->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont)); m_textEditor->setPlaceholderText(QStringLiteral("选择左侧配置文件后在此编辑")); m_textEditor->setEnabled(false); m_editorTabs->addTab(m_textEditor, QStringLiteral("原始配置")); m_fieldTree = new QTreeWidget(m_editorTabs); m_fieldTree->setColumnCount(3); m_fieldTree->setHeaderLabels({QStringLiteral("字段名"), QStringLiteral("值"), QStringLiteral("类型")}); m_fieldTree->setAlternatingRowColors(true); m_fieldTree->setRootIsDecorated(true); m_fieldTree->setUniformRowHeights(true); m_fieldTree->header()->setSectionResizeMode(0, QHeaderView::ResizeToContents); m_fieldTree->header()->setSectionResizeMode(1, QHeaderView::Stretch); m_fieldTree->header()->setSectionResizeMode(2, QHeaderView::ResizeToContents); m_fieldTree->setEditTriggers(QAbstractItemView::DoubleClicked | QAbstractItemView::EditKeyPressed | QAbstractItemView::SelectedClicked); m_editorTabs->addTab(m_fieldTree, QStringLiteral("字段编辑")); m_enableTable = new QTableWidget(m_editorTabs); m_enableTable->setColumnCount(4); m_enableTable->setHorizontalHeaderLabels( {QStringLiteral("ID"), QStringLiteral("类型"), QStringLiteral("配置文件"), QStringLiteral("启用")}); m_enableTable->horizontalHeader()->setSectionResizeMode(0, QHeaderView::ResizeToContents); m_enableTable->horizontalHeader()->setSectionResizeMode(1, QHeaderView::ResizeToContents); m_enableTable->horizontalHeader()->setSectionResizeMode(2, QHeaderView::Stretch); m_enableTable->horizontalHeader()->setSectionResizeMode(3, QHeaderView::ResizeToContents); m_enableTable->verticalHeader()->setVisible(false); m_enableTable->setSelectionBehavior(QAbstractItemView::SelectRows); m_enableTable->setEditTriggers(QAbstractItemView::NoEditTriggers); m_editorTabs->addTab(m_enableTable, QStringLiteral("启用项")); m_editorTabs->setTabEnabled(2, false); editorLayout->addWidget(m_editorTabs, 1); auto *actionLayout = new QHBoxLayout; m_statusLabel = new QLabel(QStringLiteral("未连接配置服务"), editorPanel); m_statusLabel->setObjectName(QStringLiteral("mutedLabel")); m_statusLabel->setWordWrap(true); actionLayout->addWidget(m_statusLabel, 1); m_reloadButton = new QPushButton(QStringLiteral("重新加载"), editorPanel); m_validateButton = new QPushButton(QStringLiteral("校验"), editorPanel); m_saveButton = new QPushButton(QStringLiteral("保存到设备"), editorPanel); m_saveButton->setObjectName(QStringLiteral("primaryButton")); actionLayout->addWidget(m_reloadButton); actionLayout->addWidget(m_validateButton); actionLayout->addWidget(m_saveButton); editorLayout->addLayout(actionLayout); splitter->addWidget(m_fileList); splitter->addWidget(editorPanel); splitter->setStretchFactor(0, 0); splitter->setStretchFactor(1, 1); configLayout->addWidget(splitter, 1); pageTabs->addTab(configPage, QStringLiteral("配置管理")); layout->addWidget(pageTabs, 1); m_parseTimer = new QTimer(this); m_parseTimer->setSingleShot(true); m_parseTimer->setInterval(250); connect(m_parseTimer, &QTimer::timeout, this, &ConfigEditorWidget::rebuildEnableTable); connect(m_connectButton, &QPushButton::clicked, this, &ConfigEditorWidget::toggleConnection); connect(m_refreshButton, &QPushButton::clicked, this, &ConfigEditorWidget::refreshFiles); connect(m_fileList, &QListWidget::currentItemChanged, this, &ConfigEditorWidget::selectFile); connect(m_reloadButton, &QPushButton::clicked, this, &ConfigEditorWidget::reloadCurrentFile); connect(m_validateButton, &QPushButton::clicked, this, &ConfigEditorWidget::validateCurrentFile); connect(m_saveButton, &QPushButton::clicked, this, &ConfigEditorWidget::saveCurrentFile); connect(m_textEditor, &QPlainTextEdit::textChanged, this, &ConfigEditorWidget::handleEditorChanged); connect(m_fieldTree, &QTreeWidget::itemChanged, this, &ConfigEditorWidget::handleFieldItemChanged); connect(m_deviceRefreshButton, &QPushButton::clicked, m_client, &RobotClient::requestSystemRefresh); connect(m_deviceSearchEdit, &QLineEdit::textChanged, this, &ConfigEditorWidget::rebuildDeviceCards); connect(m_deviceEditButton, &QPushButton::clicked, this, &ConfigEditorWidget::toggleDeviceEditing); connect(m_client, &RobotClient::connectionChanged, this, &ConfigEditorWidget::handleEdgeConnectionChanged); connect(m_client, &RobotClient::systemStatusReceived, this, &ConfigEditorWidget::handleSystemStatus); connect(m_client, &RobotClient::systemInfoReceived, this, &ConfigEditorWidget::handleSystemInfo); connect(m_client, &RobotClient::configurationConnectionChanged, this, &ConfigEditorWidget::handleConnectionChanged); connect(m_client, &RobotClient::configurationFilesReceived, this, &ConfigEditorWidget::handleFilesReceived); connect(m_client, &RobotClient::configurationFileReceived, this, &ConfigEditorWidget::handleFileReceived); connect(m_client, &RobotClient::configurationValidationFinished, this, &ConfigEditorWidget::handleValidationFinished); connect(m_client, &RobotClient::configurationSaveFinished, this, &ConfigEditorWidget::handleSaveFinished); rebuildDeviceCards(); updateActions(); } void ConfigEditorWidget::handleEdgeConnectionChanged(bool connected, int latencyMs, const QString &detail) { m_edgeConnected = connected; m_demoMode = connected && detail == QStringLiteral("演示数据源"); if (connected && !m_demoMode) { const QString endpoint = detail.trimmed(); const int separator = endpoint.lastIndexOf(QLatin1Char(':')); m_edgeHost = separator > 0 ? endpoint.left(separator) : endpoint; if (!m_edgeHost.isEmpty()) m_hostEdit->setText(m_edgeHost); } m_deviceRefreshButton->setEnabled(connected); if (!m_deviceEditMode && !m_overviewLoading && !m_deviceEditRequested) m_deviceEditButton->setEnabled(connected); m_edgeConnectionChip->setProperty("connected", connected); m_edgeConnectionChip->setText(connected ? QStringLiteral("● 边缘端在线 · %1 ms").arg(latencyMs) : QStringLiteral("● 边缘端未连接")); m_edgeConnectionChip->setToolTip(detail); m_edgeConnectionChip->style()->unpolish(m_edgeConnectionChip); m_edgeConnectionChip->style()->polish(m_edgeConnectionChip); if (!connected) { if (m_deviceEditMode) cancelDeviceEditing(); m_devices.clear(); m_onlineDeviceValue->setText(QStringLiteral("--")); m_deviceTypeValue->setText(QStringLiteral("--")); m_lastSyncValue->setText(QStringLiteral("--:--:--")); m_edgeSystemLabel->setText(QStringLiteral("等待获取边缘端系统信息")); rebuildDeviceCards(); return; } m_client->requestSystemRefresh(); } void ConfigEditorWidget::handleSystemStatus(double cpu, double memory, double disk, const QVariantList &devices) { Q_UNUSED(cpu) Q_UNUSED(memory) Q_UNUSED(disk) m_devices = devices; std::sort(m_devices.begin(), m_devices.end(), [](const QVariant &left, const QVariant &right) { const QVariantMap leftDevice = left.toMap(); const QVariantMap rightDevice = right.toMap(); const int leftType = leftDevice.value(QStringLiteral("type"), 20).toInt(); const int rightType = rightDevice.value(QStringLiteral("type"), 20).toInt(); if (leftType != rightType) return leftType < rightType; return leftDevice.value(QStringLiteral("deviceId")).toString() .localeAwareCompare(rightDevice.value(QStringLiteral("deviceId")).toString()) < 0; }); QSet types; for (const QVariant &entry : m_devices) types.insert(entry.toMap().value(QStringLiteral("type"), 20).toInt()); m_onlineDeviceValue->setText(QString::number(m_devices.size())); m_deviceTypeValue->setText(QString::number(types.size())); m_lastSyncValue->setText(QDateTime::currentDateTime().toString(QStringLiteral("HH:mm:ss"))); if (m_deviceEditMode) { QSet onlineIds; for (const QVariant &value : m_devices) onlineIds.insert(value.toMap().value(QStringLiteral("deviceId")).toString()); for (int index = 0; index < m_editDevices.size(); ++index) { QVariantMap item = m_editDevices.at(index).toMap(); item.insert(QStringLiteral("online"), onlineIds.contains(item.value(QStringLiteral("deviceId")).toString())); m_editDevices[index] = item; } // 编辑中的控件不能随每秒一次的 GetSystemStatus 被销毁重建,否则 // QComboBox 的弹出列表和正在输入的文本都会自动关闭或丢失焦点。 return; } rebuildDeviceCards(); } void ConfigEditorWidget::handleSystemInfo(const QVariantMap &info) { const QString name = info.value(QStringLiteral("name"), QStringLiteral("cmvr-es")).toString(); const QString version = info.value(QStringLiteral("version"), QStringLiteral("--")).toString(); const QString description = info.value(QStringLiteral("description")).toString().trimmed(); QString identity = QStringLiteral("%1 · %2").arg(name, version); if (!description.isEmpty()) identity += QStringLiteral(" · %1").arg(description); m_edgeSystemLabel->setText(identity); } void ConfigEditorWidget::rebuildDeviceCards() { while (QLayoutItem *item = m_deviceGrid->takeAt(0)) { if (item->widget()) item->widget()->deleteLater(); delete item; } const QString filter = m_deviceSearchEdit ? m_deviceSearchEdit->text().trimmed() : QString(); const QVariantList sourceDevices = m_deviceEditMode ? m_editDevices : m_devices; QVariantList visibleDevices; for (const QVariant &entry : sourceDevices) { const QVariantMap device = entry.toMap(); const int managerType = device.value(QStringLiteral("managerType"), managerTypeForApiType(device.value(QStringLiteral("type"), 20).toInt())).toInt(); const DevicePresentation presentation = presentationForManagerType( managerType, device.value(QStringLiteral("type"), 20).toInt()); const QString searchable = device.value(QStringLiteral("deviceId")).toString() + QLatin1Char(' ') + presentation.name + QLatin1Char(' ') + device.value(QStringLiteral("typeName")).toString() + QLatin1Char(' ') + device.value(QStringLiteral("deviceKindName")).toString() + QLatin1Char(' ') + device.value(QStringLiteral("driverType")).toString() + QLatin1Char(' ') + device.value(QStringLiteral("vendor")).toString(); if (filter.isEmpty() || searchable.contains(filter, Qt::CaseInsensitive)) visibleDevices.append(device); } m_deviceSectionTitle->setText(filter.isEmpty() ? QStringLiteral("在线设备 · %1").arg(sourceDevices.size()) : QStringLiteral("搜索结果 · %1").arg(visibleDevices.size())); if (visibleDevices.isEmpty() && !m_deviceEditMode) { auto *emptyState = new QFrame(m_deviceGridHost); emptyState->setObjectName(QStringLiteral("deviceEmptyState")); auto *emptyLayout = new QVBoxLayout(emptyState); emptyLayout->setContentsMargins(20, 34, 20, 34); emptyLayout->setAlignment(Qt::AlignCenter); auto *emptyIcon = new QLabel(emptyState); emptyIcon->setPixmap(UiIcons::tintedPixmap(QStringLiteral(":/icons/radio-tower.svg"), QColor(QStringLiteral("#91a0ad")), 34)); emptyIcon->setAlignment(Qt::AlignCenter); auto *emptyTitle = new QLabel(emptyState); emptyTitle->setObjectName(QStringLiteral("deviceEmptyTitle")); emptyTitle->setAlignment(Qt::AlignCenter); auto *emptyHint = new QLabel(emptyState); emptyHint->setObjectName(QStringLiteral("deviceEmptyHint")); emptyHint->setAlignment(Qt::AlignCenter); if (!filter.isEmpty()) { emptyTitle->setText(QStringLiteral("没有匹配的设备")); emptyHint->setText(QStringLiteral("请尝试其他设备 ID 或类型关键词")); } else if (!m_edgeConnected) { emptyTitle->setText(QStringLiteral("等待连接边缘端")); emptyHint->setText(QStringLiteral("请先在顶部连接 cmvr-es gRPC 服务")); } else { emptyTitle->setText(QStringLiteral("暂未发现在线设备")); emptyHint->setText(QStringLiteral("GetSystemStatus 尚未返回已注册设备")); } emptyLayout->addWidget(emptyIcon); emptyLayout->addWidget(emptyTitle); emptyLayout->addWidget(emptyHint); m_deviceGrid->addWidget(emptyState, 0, 0, 1, 3); return; } constexpr int columns = 3; for (int index = 0; index < visibleDevices.size(); ++index) { const QVariantMap device = visibleDevices.at(index).toMap(); const QString uid = device.value(QStringLiteral("uid")).toString(); const QString deviceId = device.value(QStringLiteral("deviceId"), QStringLiteral("--")).toString(); const int deviceType = device.value(QStringLiteral("type"), 20).toInt(); const int managerType = device.value(QStringLiteral("managerType"), managerTypeForApiType(deviceType)).toInt(); const DevicePresentation presentation = presentationForManagerType(managerType, deviceType); auto *card = new DeviceCardFrame(m_deviceGridHost); card->setObjectName(QStringLiteral("deviceCard")); card->setProperty("accent", presentation.accent); card->setProperty("editing", m_deviceEditMode); card->setMinimumHeight(m_deviceEditMode ? 218 : 142); auto *cardLayout = new QVBoxLayout(card); cardLayout->setContentsMargins(16, 14, 16, 14); cardLayout->setSpacing(9); auto *cardHeader = new QHBoxLayout; auto *iconBadge = new QLabel(card); iconBadge->setObjectName(QStringLiteral("deviceIconBadge")); iconBadge->setFixedSize(38, 38); iconBadge->setAlignment(Qt::AlignCenter); iconBadge->setPixmap(UiIcons::tintedPixmap(presentation.icon, QColor(presentation.accent), 20)); auto *typeLayout = new QVBoxLayout; typeLayout->setSpacing(1); auto *typeLabel = new QLabel(presentation.name, card); typeLabel->setObjectName(QStringLiteral("deviceCardType")); QString deviceMetadata = device.value(QStringLiteral("vendor")).toString().trimmed(); const QString driverType = device.value(QStringLiteral("driverType")).toString().trimmed(); if (!driverType.isEmpty()) { if (!deviceMetadata.isEmpty()) deviceMetadata += QStringLiteral(" · "); deviceMetadata += driverType; } if (deviceMetadata.isEmpty()) deviceMetadata = QStringLiteral("gRPC DEVICE"); auto *protocolLabel = new QLabel(deviceMetadata, card); protocolLabel->setObjectName(QStringLiteral("deviceCardProtocol")); protocolLabel->setToolTip(deviceMetadata); typeLayout->addWidget(typeLabel); typeLayout->addWidget(protocolLabel); auto *onlineChip = new QLabel(QStringLiteral("● 在线"), card); onlineChip->setObjectName(QStringLiteral("deviceOnlineChip")); onlineChip->setAlignment(Qt::AlignCenter); if (m_deviceEditMode && !device.value(QStringLiteral("online"), true).toBool()) { onlineChip->setText(QStringLiteral("● 待保存")); onlineChip->setProperty("pending", true); } cardHeader->addWidget(iconBadge); cardHeader->addLayout(typeLayout, 1); cardHeader->addWidget(onlineChip); if (m_deviceEditMode) { auto *deleteButton = new QToolButton(card); deleteButton->setObjectName(QStringLiteral("deviceDeleteButton")); deleteButton->setIcon(UiIcons::icon(QStringLiteral(":/icons/trash-2.svg"))); deleteButton->setToolTip(QStringLiteral("删除设备")); connect(deleteButton, &QToolButton::clicked, this, [this, uid, deviceId]() { const auto answer = QMessageBox::question( this, QStringLiteral("删除设备"), QStringLiteral("确定从 DeviceManager 配置中删除设备“%1”吗?").arg(deviceId), QMessageBox::Yes | QMessageBox::No, QMessageBox::No); if (answer != QMessageBox::Yes) return; const int editIndex = editableDeviceIndex(uid); if (editIndex >= 0) m_editDevices.removeAt(editIndex); rebuildDeviceCards(); }); cardHeader->addWidget(deleteButton); } cardLayout->addLayout(cardHeader); if (!m_deviceEditMode) { auto *idLabel = new QLabel(deviceId, card); idLabel->setObjectName(QStringLiteral("deviceCardId")); idLabel->setToolTip(deviceId); auto *sourceLabel = new QLabel(QStringLiteral("点击进入设备控制 →"), card); sourceLabel->setObjectName(QStringLiteral("deviceCardSource")); cardLayout->addWidget(idLabel); cardLayout->addWidget(sourceLabel); card->setCursor(Qt::PointingHandCursor); card->activated = [this, deviceType, managerType, deviceId]() { emit deviceNavigationRequested( managerType == 8 ? 8 : deviceType, deviceId); }; const auto transparent = [](QWidget *widget) { widget->setAttribute(Qt::WA_TransparentForMouseEvents); }; transparent(iconBadge); transparent(typeLabel); transparent(protocolLabel); transparent(onlineChip); transparent(idLabel); transparent(sourceLabel); } else { auto *fields = new QGridLayout; fields->setHorizontalSpacing(10); fields->setVerticalSpacing(5); const auto fieldLabel = [card](const QString &text) { auto *label = new QLabel(text, card); label->setObjectName(QStringLiteral("deviceConfigFieldLabel")); return label; }; fields->addWidget(fieldLabel(QStringLiteral("设备 ID")), 0, 0); fields->addWidget(fieldLabel(QStringLiteral("设备类型")), 0, 1); fields->addWidget(fieldLabel(QStringLiteral("品牌 / 驱动")), 0, 2); auto *idEdit = new QLineEdit(deviceId, card); idEdit->setObjectName(QStringLiteral("deviceConfigInput")); auto *typeCombo = new QComboBox(card); typeCombo->setObjectName(QStringLiteral("deviceConfigCombo")); const QList> deviceTypes = { {QStringLiteral("机械臂"), 12}, {QStringLiteral("相机"), 13}, {QStringLiteral("AGV"), 17}, {QStringLiteral("扬声器"), 16}, {QStringLiteral("麦克风"), 15}, {QStringLiteral("灵巧手"), 14}, {QStringLiteral("仿生头"), 8}}; for (const auto &option : deviceTypes) typeCombo->addItem(option.first, option.second); const int typeIndex = typeCombo->findData(managerType); if (typeIndex >= 0) typeCombo->setCurrentIndex(typeIndex); auto *brandCombo = new QComboBox(card); brandCombo->setObjectName(QStringLiteral("deviceConfigCombo")); const QVariantList brands = brandOptionsForType(managerType); for (const QVariant &value : brands) { const QVariantMap brand = value.toMap(); brandCombo->addItem(brand.value(QStringLiteral("label")).toString(), brand); } const QString selectedBrand = device.value(QStringLiteral("brandKey")).toString(); for (int brandIndex = 0; brandIndex < brandCombo->count(); ++brandIndex) { if (brandCombo->itemData(brandIndex).toMap().value(QStringLiteral("key")).toString() == selectedBrand) { brandCombo->setCurrentIndex(brandIndex); break; } } fields->addWidget(idEdit, 1, 0); fields->addWidget(typeCombo, 1, 1); fields->addWidget(brandCombo, 1, 2); fields->setColumnStretch(0, 5); fields->setColumnStretch(1, 4); fields->setColumnStretch(2, 5); cardLayout->addLayout(fields); auto *parameterButton = new QPushButton(card); parameterButton->setObjectName(QStringLiteral("deviceParameterToggle")); const QVariantList parameters = device.value(QStringLiteral("parameters")).toList(); const bool expanded = device.value(QStringLiteral("expanded")).toBool(); parameterButton->setText(QStringLiteral("配置参数 · %1 项").arg(parameters.size())); parameterButton->setIcon(UiIcons::icon(expanded ? QStringLiteral(":/icons/chevron-up.svg") : QStringLiteral(":/icons/chevron-down.svg"))); connect(parameterButton, &QPushButton::clicked, this, [this, uid, expanded]() { for (int row = 0; row < m_editDevices.size(); ++row) { QVariantMap item = m_editDevices.at(row).toMap(); item.insert(QStringLiteral("expanded"), false); m_editDevices[row] = item; } updateEditableDevice(uid, QStringLiteral("expanded"), !expanded, true); }); cardLayout->addWidget(parameterButton); if (expanded) { auto *parameterHost = new QWidget(card); parameterHost->setObjectName(QStringLiteral("deviceParameterHost")); auto *parameterLayout = new QGridLayout(parameterHost); parameterLayout->setContentsMargins(10, 9, 10, 9); parameterLayout->setHorizontalSpacing(10); parameterLayout->setVerticalSpacing(6); auto *nameHeader = new QLabel(QStringLiteral("字段"), parameterHost); auto *valueHeader = new QLabel(QStringLiteral("值"), parameterHost); nameHeader->setObjectName(QStringLiteral("deviceParameterHeader")); valueHeader->setObjectName(QStringLiteral("deviceParameterHeader")); parameterLayout->addWidget(nameHeader, 0, 0); parameterLayout->addWidget(valueHeader, 0, 1); for (int row = 0; row < parameters.size(); ++row) { const QVariantMap parameter = parameters.at(row).toMap(); auto *nameLabel = new QLabel( parameter.value(QStringLiteral("label")).toString(), parameterHost); nameLabel->setObjectName(QStringLiteral("deviceParameterName")); nameLabel->setToolTip(parameter.value(QStringLiteral("path")).toString()); parameterLayout->addWidget(nameLabel, row + 1, 0); const int cppType = parameter.value(QStringLiteral("type")).toInt(); const QVariantList options = parameter.value(QStringLiteral("options")).toList(); if (!options.isEmpty()) { auto *input = new QComboBox(parameterHost); input->setObjectName(QStringLiteral("deviceParameterInput")); for (const QVariant &option : options) input->addItem(option.toString()); input->setCurrentText(parameter.value(QStringLiteral("value")).toString()); connect(input, &QComboBox::currentTextChanged, this, [this, uid, row](const QString &value) { const int deviceIndex = editableDeviceIndex(uid); if (deviceIndex < 0) return; QVariantMap item = m_editDevices.at(deviceIndex).toMap(); QVariantList rows = item.value(QStringLiteral("parameters")).toList(); if (row >= rows.size()) return; QVariantMap parameter = rows.at(row).toMap(); parameter.insert(QStringLiteral("value"), value); rows[row] = parameter; item.insert(QStringLiteral("parameters"), rows); m_editDevices[deviceIndex] = item; }); parameterLayout->addWidget(input, row + 1, 1); } else if (cppType == google::protobuf::FieldDescriptor::CPPTYPE_BOOL) { auto *input = new QCheckBox(parameterHost); input->setObjectName(QStringLiteral("deviceParameterCheck")); input->setChecked(parameter.value(QStringLiteral("value")).toString() == QStringLiteral("true")); connect(input, &QCheckBox::toggled, this, [this, uid, row](bool checked) { const int deviceIndex = editableDeviceIndex(uid); if (deviceIndex < 0) return; QVariantMap item = m_editDevices.at(deviceIndex).toMap(); QVariantList rows = item.value(QStringLiteral("parameters")).toList(); QVariantMap parameter = rows.at(row).toMap(); parameter.insert(QStringLiteral("value"), checked ? QStringLiteral("true") : QStringLiteral("false")); rows[row] = parameter; item.insert(QStringLiteral("parameters"), rows); m_editDevices[deviceIndex] = item; }); parameterLayout->addWidget(input, row + 1, 1, Qt::AlignLeft); } else { auto *input = new QLineEdit( parameter.value(QStringLiteral("value")).toString(), parameterHost); input->setObjectName(QStringLiteral("deviceParameterInput")); if (parameter.value(QStringLiteral("secret")).toBool()) input->setEchoMode(QLineEdit::PasswordEchoOnEdit); connect(input, &QLineEdit::textChanged, this, [this, uid, row](const QString &value) { const int deviceIndex = editableDeviceIndex(uid); if (deviceIndex < 0) return; QVariantMap item = m_editDevices.at(deviceIndex).toMap(); QVariantList rows = item.value(QStringLiteral("parameters")).toList(); if (row >= rows.size()) return; QVariantMap parameter = rows.at(row).toMap(); parameter.insert(QStringLiteral("value"), value); rows[row] = parameter; item.insert(QStringLiteral("parameters"), rows); m_editDevices[deviceIndex] = item; }); parameterLayout->addWidget(input, row + 1, 1); } } parameterLayout->setColumnStretch(0, 4); parameterLayout->setColumnStretch(1, 6); auto *parameterScroll = new QScrollArea(card); parameterScroll->setObjectName(QStringLiteral("deviceParameterScroll")); parameterScroll->setFrameShape(QFrame::NoFrame); parameterScroll->setWidgetResizable(true); parameterScroll->setWidget(parameterHost); parameterScroll->setMinimumHeight(90); parameterScroll->setMaximumHeight(250); cardLayout->addWidget(parameterScroll); } connect(idEdit, &QLineEdit::textChanged, this, [this, uid](const QString &value) { updateEditableDevice(uid, QStringLiteral("deviceId"), value.trimmed()); }); connect(typeCombo, QOverload::of(&QComboBox::activated), this, [this, uid, typeCombo](int comboIndex) { const int editIndex = editableDeviceIndex(uid); if (editIndex < 0 || comboIndex < 0) return; QVariantMap item = m_editDevices.at(editIndex).toMap(); const int nextManagerType = typeCombo->itemData(comboIndex).toInt(); item.insert(QStringLiteral("managerType"), nextManagerType); item.insert(QStringLiteral("type"), apiTypeForManagerType(nextManagerType)); m_editDevices[editIndex] = item; refreshEditableDeviceSchema(editIndex); QTimer::singleShot(0, this, &ConfigEditorWidget::rebuildDeviceCards); }); connect(brandCombo, QOverload::of(&QComboBox::activated), this, [this, uid, brandCombo](int comboIndex) { const int editIndex = editableDeviceIndex(uid); if (editIndex < 0 || comboIndex < 0) return; QVariantMap item = m_editDevices.at(editIndex).toMap(); const QVariantMap brand = brandCombo->itemData(comboIndex).toMap(); item.insert(QStringLiteral("brandKey"), brand.value(QStringLiteral("key"))); item.insert(QStringLiteral("backend"), brand.value(QStringLiteral("backend"))); item.insert(QStringLiteral("brandNumber"), brand.value(QStringLiteral("brandNumber"), -1)); item.insert(QStringLiteral("configFile"), brand.value(QStringLiteral("configFile"))); m_editDevices[editIndex] = item; refreshEditableDeviceSchema(editIndex); QTimer::singleShot(0, this, &ConfigEditorWidget::rebuildDeviceCards); }); } m_deviceGrid->addWidget(card, index / columns, index % columns); } if (m_deviceEditMode && filter.isEmpty()) { auto *addCard = new DeviceCardFrame(m_deviceGridHost); addCard->setObjectName(QStringLiteral("deviceAddCard")); addCard->setCursor(Qt::PointingHandCursor); addCard->setMinimumHeight(218); auto *addLayout = new QVBoxLayout(addCard); addLayout->setAlignment(Qt::AlignCenter); auto *addIcon = new QLabel(addCard); addIcon->setObjectName(QStringLiteral("deviceAddIcon")); addIcon->setPixmap(UiIcons::tintedPixmap(QStringLiteral(":/icons/plus.svg"), QColor(QStringLiteral("#337ee8")), 30)); addIcon->setAlignment(Qt::AlignCenter); auto *addTitle = new QLabel(QStringLiteral("增加新设备"), addCard); addTitle->setObjectName(QStringLiteral("deviceAddTitle")); auto *addHint = new QLabel(QStringLiteral("添加 DeviceManager 配置项"), addCard); addHint->setObjectName(QStringLiteral("deviceAddHint")); addHint->setAlignment(Qt::AlignCenter); addLayout->addWidget(addIcon); addLayout->addWidget(addTitle, 0, Qt::AlignCenter); addLayout->addWidget(addHint, 0, Qt::AlignCenter); addCard->activated = [this]() { addEditableDevice(); }; addIcon->setAttribute(Qt::WA_TransparentForMouseEvents); addTitle->setAttribute(Qt::WA_TransparentForMouseEvents); addHint->setAttribute(Qt::WA_TransparentForMouseEvents); const int addIndex = visibleDevices.size(); m_deviceGrid->addWidget(addCard, addIndex / columns, addIndex % columns); } for (int column = 0; column < columns; ++column) m_deviceGrid->setColumnStretch(column, 1); } QVariantList ConfigEditorWidget::brandOptionsForType(int managerType) const { const QString folder = typeFolderForManagerType(managerType); const auto resolvePath = [this, folder](const QString &preferred) { for (const QVariant &value : m_configurationFiles) { if (value.toMap().value(QStringLiteral("relativePath")).toString() == preferred) return preferred; } for (const QVariant &value : m_configurationFiles) { const QString path = value.toMap().value(QStringLiteral("relativePath")).toString(); if (path.contains(QStringLiteral("/devices/%1/").arg(folder)) || path.startsWith(QStringLiteral("devices/%1/").arg(folder))) return path; } return preferred; }; const auto option = [](const QString &key, const QString &label, const QString &backend, int brandNumber, const QString &configFile) { QVariantMap value; value.insert(QStringLiteral("key"), key); value.insert(QStringLiteral("label"), label); value.insert(QStringLiteral("backend"), backend); value.insert(QStringLiteral("brandNumber"), brandNumber); value.insert(QStringLiteral("configFile"), configFile); return value; }; QVariantList result; switch (managerType) { case 17: { const QString path = resolvePath(QStringLiteral("devices/agv/agv.pb.txt")); result << option(QStringLiteral("seer"), QStringLiteral("仙工 · Robokit / SRC1100"), QStringLiteral("seer_robokit_agv"), -1, path) << option(QStringLiteral("generic_agv"), QStringLiteral("通用 / 测试 AGV"), QStringLiteral("my_agv"), -1, path); break; } case 12: result << option(QStringLiteral("aubo"), QStringLiteral("AUBO"), QStringLiteral("vendor"), 1, resolvePath(QStringLiteral("devices/arm/aubo_arm.pb.txt"))) << option(QStringLiteral("huayan"), QStringLiteral("华沿 Elfin"), QStringLiteral("vendor"), 2, resolvePath(QStringLiteral("devices/arm/huayan_arm.pb.txt"))); for (const QVariant &value : m_configurationFiles) { const QString path = value.toMap().value(QStringLiteral("relativePath")).toString(); if (!path.contains(QStringLiteral("devices/arm/")) || path.endsWith(QStringLiteral("aubo_arm.pb.txt")) || path.endsWith(QStringLiteral("huayan_arm.pb.txt"))) continue; QString name = QFileInfo(path).fileName(); name.remove(QStringLiteral(".pb.txt")); result << option(QStringLiteral("motor:%1").arg(path), QStringLiteral("电机 / %1").arg(name), QStringLiteral("motor"), -1, path); } if (result.size() == 2) { result << option(QStringLiteral("motor"), QStringLiteral("电机 / 通用机械臂"), QStringLiteral("motor"), -1, resolvePath(QStringLiteral("devices/arm/arm.pb.txt"))); } break; case 13: { const QString path = resolvePath(QStringLiteral("devices/camera/camera.pb.txt")); result << option(QStringLiteral("hikvision"), QStringLiteral("海康威视"), QStringLiteral("hikvision"), -1, path) << option(QStringLiteral("realsense"), QStringLiteral("Intel RealSense"), QStringLiteral("realsense"), -1, path) << option(QStringLiteral("mechmind"), QStringLiteral("梅卡曼德 Mech-Mind"), QStringLiteral("mechmind"), -1, path) << option(QStringLiteral("uvc"), QStringLiteral("UVC / USB 相机"), QStringLiteral("uvc"), -1, path) << option(QStringLiteral("mujoco_camera"), QStringLiteral("MuJoCo 虚拟相机"), QStringLiteral("mujoco"), -1, path); break; } case 14: { const QString path = resolvePath(QStringLiteral("devices/dexhand/dexhand.pb.txt")); result << option(QStringLiteral("rh56dftp"), QStringLiteral("因时 RH56DFTP"), QStringLiteral("rh56dftp"), -1, path) << option(QStringLiteral("px6ax"), QStringLiteral("PX-6AX Gen3"), QStringLiteral("px_6ax_gen3"), -1, path) << option(QStringLiteral("zero_sim_touch"), QStringLiteral("ZeroSimTouch"), QStringLiteral("zero_sim_touch"), -1, path); break; } case 15: result << option(QStringLiteral("ffmpeg_microphone"), QStringLiteral("FFmpeg / USB Audio"), QStringLiteral("ffmpeg"), -1, resolvePath(QStringLiteral("devices/microphone/microphone.pb.txt"))); break; case 16: result << option(QStringLiteral("ffmpeg_speaker"), QStringLiteral("FFmpeg / USB Audio"), QStringLiteral("ffmpeg"), -1, resolvePath(QStringLiteral("devices/speaker/speaker.pb.txt"))); break; case 8: result << option(QStringLiteral("biohead"), QStringLiteral("串口仿生头"), QString(), -1, resolvePath(QStringLiteral("devices/biohead/bio_head.pb.txt"))); break; default: break; } return result; } QVariantList ConfigEditorWidget::parameterRowsForDevice(const QVariantMap &device) const { const QString path = device.value(QStringLiteral("configFile")).toString(); const int managerType = device.value(QStringLiteral("managerType")).toInt(); const QString messageType = m_overviewMessageTypes.value( path, defaultMessageTypeForManagerType(managerType)); QString error; auto root = createMessage(messageType, m_overviewContents.value(path), &error); if (!root) return {}; auto *entry = deviceEntry(root.get(), device.value(QStringLiteral("deviceId")).toString(), true); if (!entry) return {}; if (const auto *idField = entry->GetDescriptor()->FindFieldByName("id")) entry->GetReflection()->SetString(entry, idField, device.value(QStringLiteral("deviceId")).toString().toStdString()); auto *backend = selectBackend(entry, device.value(QStringLiteral("backend")).toString(), device.value(QStringLiteral("brandNumber"), -1).toInt()); if (!backend) return {}; if (const auto *idField = backend->GetDescriptor()->FindFieldByName("id")) backend->GetReflection()->SetString(backend, idField, device.value(QStringLiteral("deviceId")).toString().toStdString()); QVariantList rows; appendParameterRows(entry, QString(), &rows); return rows; } int ConfigEditorWidget::editableDeviceIndex(const QString &uid) const { for (int index = 0; index < m_editDevices.size(); ++index) { if (m_editDevices.at(index).toMap().value(QStringLiteral("uid")).toString() == uid) return index; } return -1; } void ConfigEditorWidget::updateEditableDevice(const QString &uid, const QString &key, const QVariant &value, bool rebuild) { const int index = editableDeviceIndex(uid); if (index < 0) return; QVariantMap device = m_editDevices.at(index).toMap(); device.insert(key, value); m_editDevices[index] = device; if (rebuild) rebuildDeviceCards(); } void ConfigEditorWidget::refreshEditableDeviceSchema(int index) { if (index < 0 || index >= m_editDevices.size()) return; QVariantMap device = m_editDevices.at(index).toMap(); const QVariantList brands = brandOptionsForType( device.value(QStringLiteral("managerType")).toInt()); QVariantMap selected; const QString key = device.value(QStringLiteral("brandKey")).toString(); for (const QVariant &value : brands) { if (value.toMap().value(QStringLiteral("key")).toString() == key) { selected = value.toMap(); break; } } if (selected.isEmpty() && !brands.isEmpty()) selected = brands.first().toMap(); device.insert(QStringLiteral("brandKey"), selected.value(QStringLiteral("key"))); device.insert(QStringLiteral("backend"), selected.value(QStringLiteral("backend"))); device.insert(QStringLiteral("brandNumber"), selected.value(QStringLiteral("brandNumber"), -1)); device.insert(QStringLiteral("configFile"), selected.value(QStringLiteral("configFile"))); device.insert(QStringLiteral("parameters"), parameterRowsForDevice(device)); m_editDevices[index] = device; } void ConfigEditorWidget::addEditableDevice() { QVariantMap device; device.insert(QStringLiteral("uid"), QUuid::createUuid().toString(QUuid::WithoutBraces)); int suffix = 1; QString id; QSet used; for (const QVariant &value : m_editDevices) used.insert(value.toMap().value(QStringLiteral("deviceId")).toString()); do { id = QStringLiteral("new_robot_%1").arg(suffix++); } while (used.contains(id)); device.insert(QStringLiteral("deviceId"), id); device.insert(QStringLiteral("originalId"), QString()); device.insert(QStringLiteral("managerType"), 12); device.insert(QStringLiteral("type"), 6); device.insert(QStringLiteral("online"), false); device.insert(QStringLiteral("enable"), true); device.insert(QStringLiteral("expanded"), true); m_editDevices.append(device); refreshEditableDeviceSchema(m_editDevices.size() - 1); rebuildDeviceCards(); } void ConfigEditorWidget::buildEditableDevices() { m_editDevices.clear(); cmvr::config::DeviceManagerRootConfig manager; const bool managerReady = m_managerContent.trimmed().isEmpty() || google::protobuf::TextFormat::ParseFromString(m_managerContent.toStdString(), &manager); if (!managerReady) { QMessageBox::critical(this, QStringLiteral("读取设备配置"), QStringLiteral("DeviceManager 配置格式与当前上位机协议不兼容。")); cancelDeviceEditing(); return; } for (const QVariant &value : m_devices) { const QVariantMap live = value.toMap(); const QString id = live.value(QStringLiteral("deviceId")).toString(); QVariantMap device = live; device.insert(QStringLiteral("uid"), QUuid::createUuid().toString(QUuid::WithoutBraces)); device.insert(QStringLiteral("originalId"), id); device.insert(QStringLiteral("online"), true); device.insert(QStringLiteral("expanded"), false); int managerType = managerTypeForApiType(live.value(QStringLiteral("type"), 20).toInt()); QString configFile; bool enabled = true; for (const auto &entry : manager.device_manager().devices()) { if (QString::fromStdString(entry.id()) != id) continue; managerType = static_cast(entry.type()); configFile = QString::fromStdString(entry.config_file()); enabled = entry.enable(); break; } device.insert(QStringLiteral("managerType"), managerType); device.insert(QStringLiteral("type"), apiTypeForManagerType(managerType)); device.insert(QStringLiteral("configFile"), configFile); device.insert(QStringLiteral("enable"), enabled); const QVariantList brands = brandOptionsForType(managerType); QVariantMap selected; QString activeBackend; int activeBrand = -1; if (!configFile.isEmpty()) { const QString messageType = m_overviewMessageTypes.value( configFile, defaultMessageTypeForManagerType(managerType)); auto root = createMessage(messageType, m_overviewContents.value(configFile)); auto *entry = deviceEntry(root.get(), id, false); if (entry) { if (const auto *oneof = entry->GetDescriptor()->FindOneofByName("backend")) { const auto *active = entry->GetReflection()->GetOneofFieldDescriptor(*entry, oneof); if (active) { activeBackend = QString::fromStdString(active->name()); const auto &backend = entry->GetReflection()->GetMessage(*entry, active); const auto *brandField = backend.GetDescriptor()->FindFieldByName("brand"); if (brandField) activeBrand = backend.GetReflection()->GetEnumValue(backend, brandField); } } } } for (const QVariant &brandValue : brands) { const QVariantMap brand = brandValue.toMap(); const bool samePath = configFile.isEmpty() || brand.value(QStringLiteral("configFile")).toString() == configFile; const bool sameBackend = activeBackend.isEmpty() || brand.value(QStringLiteral("backend")).toString() == activeBackend; const int brandNumber = brand.value(QStringLiteral("brandNumber"), -1).toInt(); const bool sameBrand = activeBrand < 0 || brandNumber < 0 || brandNumber == activeBrand; if (samePath && sameBackend && sameBrand) { selected = brand; break; } } if (selected.isEmpty() && !brands.isEmpty()) selected = brands.first().toMap(); device.insert(QStringLiteral("brandKey"), selected.value(QStringLiteral("key"))); device.insert(QStringLiteral("backend"), selected.value(QStringLiteral("backend"))); device.insert(QStringLiteral("brandNumber"), selected.value(QStringLiteral("brandNumber"), -1)); if (configFile.isEmpty()) device.insert(QStringLiteral("configFile"), selected.value(QStringLiteral("configFile"))); device.insert(QStringLiteral("parameters"), parameterRowsForDevice(device)); m_editDevices.append(device); } m_deviceEditMode = true; m_deviceEditRequested = false; m_overviewLoading = false; m_deviceEditButton->setEnabled(true); m_deviceEditButton->setText(QStringLiteral("完成")); m_deviceEditButton->setIcon(UiIcons::icon(QStringLiteral(":/icons/check.svg"))); m_deviceEditButton->setObjectName(QStringLiteral("deviceEditButtonActive")); m_deviceSectionHint->setText( QStringLiteral("正在编辑 DeviceManager 配置;完成后将保存并上传到 cmvr-es")); m_deviceSearchEdit->clear(); rebuildDeviceCards(); } void ConfigEditorWidget::beginDeviceConfigurationLoad() { m_overviewLoading = true; m_deviceEditRequested = false; m_deviceEditButton->setEnabled(false); m_deviceEditButton->setText(QStringLiteral("加载中…")); m_configurationFiles.clear(); m_overviewContents.clear(); m_overviewRevisions.clear(); m_overviewMessageTypes.clear(); m_client->requestConfigurationFiles(); } void ConfigEditorWidget::readNextOverviewConfiguration() { if (m_overviewReadQueue.isEmpty()) { m_overviewCurrentRead.clear(); buildEditableDevices(); return; } m_overviewCurrentRead = m_overviewReadQueue.takeFirst(); m_client->readConfigurationFile(m_overviewCurrentRead); } void ConfigEditorWidget::toggleDeviceEditing() { if (m_deviceEditMode) { finishDeviceEditing(); return; } if (!m_edgeConnected) { QMessageBox::information(this, QStringLiteral("设备配置"), QStringLiteral("请先连接 cmvr-es,再修改设备配置。")); return; } if (m_demoMode) { m_managerContent.clear(); m_configurationFiles.clear(); m_overviewContents.clear(); m_overviewMessageTypes.clear(); buildEditableDevices(); return; } if (!m_connected) { m_deviceEditRequested = true; m_deviceEditButton->setEnabled(false); m_deviceEditButton->setText(QStringLiteral("连接配置服务…")); const QString host = m_edgeHost.isEmpty() ? m_hostEdit->text().trimmed() : m_edgeHost; m_client->connectConfigurationServer(host, m_portSpin->value()); return; } beginDeviceConfigurationLoad(); } void ConfigEditorWidget::cancelDeviceEditing() { m_deviceEditMode = false; m_deviceEditRequested = false; m_overviewLoading = false; m_overviewSaving = false; m_editDevices.clear(); m_deviceEditButton->setEnabled(m_edgeConnected); m_deviceEditButton->setText(QStringLiteral("修改")); m_deviceEditButton->setIcon(UiIcons::icon(QStringLiteral(":/icons/edit-3.svg"))); m_deviceEditButton->setObjectName(QStringLiteral("deviceEditButton")); m_deviceSectionHint->setText( QStringLiteral("设备 ID 与类型由边缘端 DeviceManager 注册信息生成")); rebuildDeviceCards(); } bool ConfigEditorWidget::prepareOverviewSave(QString *error) { cmvr::config::DeviceManagerRootConfig manager; if (!google::protobuf::TextFormat::ParseFromString(m_managerContent.toStdString(), &manager)) { if (error) *error = QStringLiteral("无法解析 DeviceManager 配置"); return false; } QSet originalIds; for (const QVariant &value : m_devices) originalIds.insert(value.toMap().value(QStringLiteral("deviceId")).toString()); QSet currentIds; for (const QVariant &value : m_editDevices) { const QString id = value.toMap().value(QStringLiteral("deviceId")).toString().trimmed(); if (id.isEmpty()) { if (error) *error = QStringLiteral("设备 ID 不能为空"); return false; } if (currentIds.contains(id)) { if (error) *error = QStringLiteral("设备 ID “%1” 重复").arg(id); return false; } currentIds.insert(id); } QSet affectedPaths; auto *entries = manager.mutable_device_manager()->mutable_devices(); for (int index = entries->size() - 1; index >= 0; --index) { if (!originalIds.contains(QString::fromStdString(entries->Get(index).id()))) continue; affectedPaths.insert(QString::fromStdString(entries->Get(index).config_file())); entries->DeleteSubrange(index, 1); } for (const QVariant &value : m_editDevices) { const QVariantMap device = value.toMap(); const QString path = device.value(QStringLiteral("configFile")).toString(); if (path.isEmpty()) { if (error) *error = QStringLiteral("设备 “%1” 没有可用配置文件") .arg(device.value(QStringLiteral("deviceId")).toString()); return false; } affectedPaths.insert(path); auto *entry = entries->Add(); entry->set_id(device.value(QStringLiteral("deviceId")).toString().toStdString()); entry->set_type(static_cast( device.value(QStringLiteral("managerType")).toInt())); entry->set_config_file(path.toStdString()); entry->set_enable(device.value(QStringLiteral("enable"), true).toBool()); } m_overviewSaveContents.clear(); m_overviewSaveRevisions.clear(); for (const QString &path : std::as_const(affectedPaths)) { if (path.isEmpty()) continue; if (!m_overviewContents.contains(path) || !m_overviewRevisions.contains(path)) { if (error) *error = QStringLiteral("尚未加载配置文件 %1,无法安全保存").arg(path); return false; } int managerType = 0; for (const QVariant &value : m_editDevices) { if (value.toMap().value(QStringLiteral("configFile")).toString() == path) { managerType = value.toMap().value(QStringLiteral("managerType")).toInt(); break; } } const QString messageType = m_overviewMessageTypes.value( path, defaultMessageTypeForManagerType(managerType)); QString parseError; auto root = createMessage(messageType, m_overviewContents.value(path), &parseError); if (!root) { if (error) *error = QStringLiteral("%1:%2").arg(path, parseError); return false; } removeDeviceEntries(root.get(), originalIds); int directEntryCount = 0; for (const QVariant &value : m_editDevices) { const QVariantMap device = value.toMap(); if (device.value(QStringLiteral("configFile")).toString() != path) continue; if (device.value(QStringLiteral("managerType")).toInt() == 8 && ++directEntryCount > 1) { if (error) *error = QStringLiteral("仿生头配置文件当前只支持一个设备"); return false; } const QString id = device.value(QStringLiteral("deviceId")).toString().trimmed(); auto *entry = deviceEntry(root.get(), id, true); if (!entry) { if (error) *error = QStringLiteral("无法在 %1 中创建设备 %2").arg(path, id); return false; } if (const auto *idField = entry->GetDescriptor()->FindFieldByName("id")) entry->GetReflection()->SetString(entry, idField, id.toStdString()); auto *backend = selectBackend(entry, device.value(QStringLiteral("backend")).toString(), device.value(QStringLiteral("brandNumber"), -1).toInt()); if (!backend) { if (error) *error = QStringLiteral("设备 %1 的品牌驱动与配置协议不匹配").arg(id); return false; } if (const auto *idField = backend->GetDescriptor()->FindFieldByName("id")) backend->GetReflection()->SetString(backend, idField, id.toStdString()); const QVariantList parameters = device.value(QStringLiteral("parameters")).toList(); for (const QVariant ¶meterValue : parameters) { const QVariantMap parameter = parameterValue.toMap(); const QString parameterPath = parameter.value(QStringLiteral("path")).toString(); const QString parameterText = parameter.value(QStringLiteral("value")).toString(); if (parameterText.isEmpty() && parameterPath.endsWith(QLatin1Char(']'))) continue; if (!setParameterValue(entry, parameterPath, parameterText)) { if (error) *error = QStringLiteral("设备 %1 的参数 %2 格式不正确") .arg(id, parameterPath); return false; } } } const QString serialized = printMessage(*root); if (serialized.isEmpty()) { if (error) *error = QStringLiteral("无法序列化 %1").arg(path); return false; } m_overviewSaveContents.insert(path, serialized); m_overviewSaveRevisions.insert(path, m_overviewRevisions.value(path)); } const QString managerSerialized = printMessage(manager); if (managerSerialized.isEmpty()) { if (error) *error = QStringLiteral("无法序列化 DeviceManager 配置"); return false; } m_overviewSaveContents.insert(m_managerPath, managerSerialized); m_overviewSaveRevisions.insert(m_managerPath, m_managerRevision); m_overviewSaveQueue = m_overviewSaveContents.keys(); m_overviewSaveQueue.removeAll(m_managerPath); std::sort(m_overviewSaveQueue.begin(), m_overviewSaveQueue.end()); m_overviewSaveQueue.append(m_managerPath); return true; } void ConfigEditorWidget::finishDeviceEditing() { if (m_demoMode) { QVariantList demoDevices; for (const QVariant &value : m_editDevices) { const QVariantMap configured = value.toMap(); QVariantMap live; live.insert(QStringLiteral("deviceId"), configured.value(QStringLiteral("deviceId"))); live.insert(QStringLiteral("type"), configured.value(QStringLiteral("type"))); live.insert(QStringLiteral("typeName"), QStringLiteral("DEVICE_TYPE_DEMO")); demoDevices.append(live); } m_devices = demoDevices; cancelDeviceEditing(); QMessageBox::information(this, QStringLiteral("演示模式"), QStringLiteral("演示配置已应用到当前界面;未写入远端文件。")); return; } QString error; if (!prepareOverviewSave(&error)) { QMessageBox::warning(this, QStringLiteral("无法保存设备配置"), error); return; } m_overviewSaving = true; m_deviceEditButton->setEnabled(false); m_deviceEditButton->setText(QStringLiteral("保存中…")); saveNextOverviewConfiguration(); } void ConfigEditorWidget::saveNextOverviewConfiguration() { if (m_overviewSaveQueue.isEmpty()) { m_overviewSaving = false; m_overviewCurrentSave.clear(); cancelDeviceEditing(); m_client->requestSystemRefresh(); QMessageBox::information( this, QStringLiteral("设备配置已保存"), QStringLiteral("配置文件已上传到 cmvr-es。配置重载/重启功能将在后续接入。")); return; } m_overviewCurrentSave = m_overviewSaveQueue.takeFirst(); m_client->saveConfigurationFile( m_overviewCurrentSave, m_overviewSaveContents.value(m_overviewCurrentSave), m_overviewSaveRevisions.value(m_overviewCurrentSave)); } void ConfigEditorWidget::toggleConnection() { if (m_connected) { if (!confirmDiscardChanges()) return; m_client->disconnectConfigurationServer(); return; } if (m_hostEdit->text().trimmed().isEmpty()) { setStatus(QStringLiteral("设备地址不能为空"), true); return; } QSettings settings; settings.setValue(QStringLiteral("configuration/host"), m_hostEdit->text().trimmed()); settings.setValue(QStringLiteral("configuration/port"), m_portSpin->value()); setBusy(true); setStatus(QStringLiteral("正在连接配置服务...")); m_client->connectConfigurationServer(m_hostEdit->text(), m_portSpin->value()); } void ConfigEditorWidget::refreshFiles() { if (!m_connected || !confirmDiscardChanges()) return; setBusy(true); setStatus(QStringLiteral("正在读取远端配置列表...")); m_client->requestConfigurationFiles(); } void ConfigEditorWidget::selectFile(QListWidgetItem *current, QListWidgetItem *previous) { if (!current) return; const QString nextPath = current->data(kPathRole).toString(); if (nextPath == m_currentPath) return; if (!confirmDiscardChanges()) { const QSignalBlocker blocker(m_fileList); m_fileList->setCurrentItem(previous); return; } setBusy(true); setStatus(QStringLiteral("正在读取 %1...").arg(nextPath)); m_client->readConfigurationFile(nextPath); } void ConfigEditorWidget::reloadCurrentFile() { if (m_currentPath.isEmpty() || !confirmDiscardChanges()) return; setBusy(true); m_client->readConfigurationFile(m_currentPath); } void ConfigEditorWidget::validateCurrentFile() { if (m_currentPath.isEmpty()) return; setBusy(true); setStatus(QStringLiteral("正在校验 %1...").arg(m_currentPath)); m_client->validateConfigurationFile(m_currentPath, m_textEditor->toPlainText()); } void ConfigEditorWidget::saveCurrentFile() { if (m_currentPath.isEmpty() || m_revision.isEmpty()) return; setBusy(true); setStatus(QStringLiteral("正在保存 %1...").arg(m_currentPath)); m_client->saveConfigurationFile(m_currentPath, m_textEditor->toPlainText(), m_revision); } void ConfigEditorWidget::handleConnectionChanged(bool connected, const QString &detail) { m_connected = connected; setBusy(false); m_hostEdit->setEnabled(!connected); m_portSpin->setEnabled(!connected); m_connectButton->setText(connected ? QStringLiteral("断开") : QStringLiteral("连接")); setStatus(detail, !connected && detail != QStringLiteral("配置服务已断开")); if (connected && m_deviceEditRequested) { beginDeviceConfigurationLoad(); } else if (connected) { refreshFiles(); } else { if (m_deviceEditRequested || m_overviewLoading) { m_deviceEditRequested = false; m_overviewLoading = false; m_deviceEditButton->setEnabled(m_edgeConnected); m_deviceEditButton->setText(QStringLiteral("修改")); } m_fileList->clear(); m_rootLabel->setText(QStringLiteral("远端根目录:--")); clearCurrentFile(); } updateActions(); } void ConfigEditorWidget::handleFilesReceived(const QString &configRoot, const QVariantList &files, bool success, const QString &detail) { setBusy(false); if (!success) { if (m_overviewLoading) { m_overviewLoading = false; m_deviceEditButton->setEnabled(true); m_deviceEditButton->setText(QStringLiteral("修改")); } setStatus(detail.isEmpty() ? QStringLiteral("读取配置列表失败") : detail, true); return; } if (m_overviewLoading) { Q_UNUSED(configRoot) m_configurationFiles = files; m_overviewReadQueue.clear(); m_managerPath.clear(); const QSet supportedFolders = { QStringLiteral("agv"), QStringLiteral("arm"), QStringLiteral("camera"), QStringLiteral("dexhand"), QStringLiteral("microphone"), QStringLiteral("speaker"), QStringLiteral("biohead")}; for (const QVariant &value : files) { const QVariantMap file = value.toMap(); const QString path = file.value(QStringLiteral("relativePath")).toString(); m_overviewMessageTypes.insert(path, file.value(QStringLiteral("messageType")).toString()); if (path.endsWith(QStringLiteral("device_manager.pb.txt"))) { m_managerPath = path; continue; } const QString normalized = QDir::fromNativeSeparators(path); for (const QString &folder : supportedFolders) { if (normalized.contains(QStringLiteral("devices/%1/").arg(folder))) { m_overviewReadQueue.append(path); break; } } } if (m_managerPath.isEmpty()) { m_overviewLoading = false; m_deviceEditButton->setEnabled(true); m_deviceEditButton->setText(QStringLiteral("修改")); QMessageBox::warning(this, QStringLiteral("读取设备配置"), QStringLiteral("远端配置列表中未找到 device_manager.pb.txt。")); return; } m_overviewReadQueue.prepend(m_managerPath); readNextOverviewConfiguration(); return; } const QSignalBlocker blocker(m_fileList); m_fileList->clear(); clearCurrentFile(); for (const QVariant &value : files) { const QVariantMap file = value.toMap(); const QString path = file.value(QStringLiteral("relativePath")).toString(); auto *item = new QListWidgetItem(path, m_fileList); item->setData(kPathRole, path); item->setToolTip(QStringLiteral("%1\n%2 bytes\n%3") .arg(file.value(QStringLiteral("messageType")).toString()) .arg(file.value(QStringLiteral("sizeBytes")).toULongLong()) .arg(QDateTime::fromMSecsSinceEpoch( file.value(QStringLiteral("modifiedUnixMs")).toLongLong()).toString(Qt::ISODate))); } m_rootLabel->setText(QStringLiteral("远端根目录:%1").arg(configRoot)); setStatus(QStringLiteral("已读取 %1 个配置文件").arg(files.size())); updateActions(); } void ConfigEditorWidget::handleFileReceived(const QString &relativePath, const QString &content, const QString &revision, const QString &messageType, bool success, const QString &detail) { setBusy(false); if (m_overviewLoading && relativePath == m_overviewCurrentRead) { if (!success) { m_overviewLoading = false; m_deviceEditButton->setEnabled(true); m_deviceEditButton->setText(QStringLiteral("修改")); QMessageBox::warning(this, QStringLiteral("读取设备配置"), detail.isEmpty() ? QStringLiteral("读取 %1 失败").arg(relativePath) : detail); return; } m_overviewContents.insert(relativePath, content); m_overviewRevisions.insert(relativePath, revision); m_overviewMessageTypes.insert(relativePath, messageType); if (relativePath == m_managerPath) { m_managerContent = content; m_managerRevision = revision; m_managerMessageType = messageType; } readNextOverviewConfiguration(); return; } if (!success) { setStatus(detail.isEmpty() ? QStringLiteral("读取配置文件失败") : detail, true); return; } m_loading = true; m_currentPath = relativePath; m_revision = revision; m_messageType = messageType; m_textEditor->setPlainText(content); m_textEditor->document()->setModified(false); m_loading = false; m_dirty = false; m_pathLabel->setText(relativePath); m_typeLabel->setText(QStringLiteral("消息类型:%1 版本:%2") .arg(messageType, revision)); rebuildFieldTree(); rebuildEnableTable(); setStatus(QStringLiteral("配置已加载")); updateActions(); } void ConfigEditorWidget::handleValidationFinished(const QString &relativePath, bool success, const QString &detail) { if (relativePath != m_currentPath) return; setBusy(false); setStatus(detail.isEmpty() ? (success ? QStringLiteral("配置有效") : QStringLiteral("配置无效")) : detail, !success); } void ConfigEditorWidget::handleSaveFinished(const QString &relativePath, const QString &revision, bool success, const QString &detail) { if (m_overviewSaving && relativePath == m_overviewCurrentSave) { if (!success) { m_overviewSaving = false; m_deviceEditButton->setEnabled(true); m_deviceEditButton->setText(QStringLiteral("完成")); QMessageBox::critical( this, QStringLiteral("保存设备配置失败"), QStringLiteral("%1\n%2\n\n已成功保存的文件不会自动回滚,请刷新后再试。") .arg(relativePath, detail.isEmpty() ? QStringLiteral("远端拒绝保存") : detail)); return; } m_overviewRevisions.insert(relativePath, revision); saveNextOverviewConfiguration(); return; } if (relativePath != m_currentPath) return; setBusy(false); if (!success) { setStatus(detail.isEmpty() ? QStringLiteral("保存失败") : detail, true); return; } m_revision = revision; m_dirty = false; m_textEditor->document()->setModified(false); m_typeLabel->setText(m_typeLabel->text().section(QStringLiteral(" 版本:"), 0, 0) + QStringLiteral(" 版本:") + revision); setStatus(QStringLiteral("已保存到远端项目配置文件")); updateActions(); } void ConfigEditorWidget::handleEditorChanged() { if (m_loading || m_currentPath.isEmpty()) return; m_dirty = true; m_parseTimer->start(); updateActions(); } bool ConfigEditorWidget::parseFieldDocument() { m_dynamicMessage.reset(); m_dynamicFactory.reset(); if (m_messageType.isEmpty() || m_textEditor->toPlainText().trimmed().isEmpty()) return false; const auto *descriptor = google::protobuf::DescriptorPool::generated_pool() ->FindMessageTypeByName(m_messageType.toStdString()); if (!descriptor) return false; m_dynamicFactory = std::make_unique(); const auto *prototype = m_dynamicFactory->GetPrototype(descriptor); if (!prototype) return false; m_dynamicMessage.reset(prototype->New()); if (!google::protobuf::TextFormat::ParseFromString( m_textEditor->toPlainText().toStdString(), m_dynamicMessage.get())) { m_dynamicMessage.reset(); return false; } return true; } void ConfigEditorWidget::appendMessageFields(QTreeWidgetItem *parent, google::protobuf::Message *message) { if (!parent || !message) return; const auto *descriptor = message->GetDescriptor(); const auto *reflection = message->GetReflection(); using Field = google::protobuf::FieldDescriptor; for (int fieldIndex = 0; fieldIndex < descriptor->field_count(); ++fieldIndex) { const auto *field = descriptor->field(fieldIndex); const QString fieldName = QString::fromStdString(field->name()); if (field->is_repeated()) { const int count = reflection->FieldSize(*message, field); if (count == 0) { auto *empty = new QTreeWidgetItem(parent); empty->setText(0, fieldName + QStringLiteral(" []")); empty->setText(1, QStringLiteral("<空>")); empty->setText(2, fieldTypeName(field)); empty->setFlags(empty->flags() & ~Qt::ItemIsEditable); continue; } for (int index = 0; index < count; ++index) { auto *item = new QTreeWidgetItem(parent); item->setText(0, fieldName + QStringLiteral("[%1]").arg(index)); item->setText(2, fieldTypeName(field)); if (field->cpp_type() == Field::CPPTYPE_MESSAGE) { item->setText(1, QStringLiteral("message")); appendMessageFields(item, reflection->MutableRepeatedMessage(message, field, index)); item->setExpanded(true); } else { const QString value = scalarValue(message, field, index); item->setText(1, value); setItemMetadata(item, message, field, index, value); } } continue; } auto *item = new QTreeWidgetItem(parent); item->setText(0, fieldName); item->setText(2, fieldTypeName(field)); if (field->cpp_type() == Field::CPPTYPE_MESSAGE) { if (reflection->HasField(*message, field)) { item->setText(1, QStringLiteral("message")); appendMessageFields(item, reflection->MutableMessage(message, field)); item->setExpanded(true); } else { item->setText(1, QStringLiteral("<未设置>")); item->setFlags(item->flags() & ~Qt::ItemIsEditable); } } else { const QString value = scalarValue(message, field, -1); item->setText(1, value); setItemMetadata(item, message, field, -1, value); } } } void ConfigEditorWidget::rebuildFieldTree() { const QSignalBlocker blocker(m_fieldTree); m_fieldTree->clear(); if (!parseFieldDocument()) return; auto *root = new QTreeWidgetItem(m_fieldTree); root->setText(0, m_messageType); root->setText(1, QStringLiteral("message")); root->setText(2, QStringLiteral("message")); root->setFlags(root->flags() & ~Qt::ItemIsEditable); appendMessageFields(root, m_dynamicMessage.get()); root->setExpanded(true); } bool ConfigEditorWidget::applyFieldValue(QTreeWidgetItem *item, const QString &text) { if (!item) return false; auto *message = reinterpret_cast( pointerValue(item->data(1, kMessageRole))); const auto *field = reinterpret_cast( pointerValue(item->data(1, kFieldRole))); if (!message || !field) return false; const int index = item->data(1, kIndexRole).toInt(); const QString value = text.trimmed(); const auto *reflection = message->GetReflection(); using Field = google::protobuf::FieldDescriptor; bool ok = true; auto setBool = [&](bool parsed) { if (field->is_repeated()) reflection->SetRepeatedBool(message, field, index, parsed); else reflection->SetBool(message, field, parsed); }; switch (field->cpp_type()) { case Field::CPPTYPE_BOOL: if (value.compare(QStringLiteral("true"), Qt::CaseInsensitive) == 0 || value == QStringLiteral("1")) setBool(true); else if (value.compare(QStringLiteral("false"), Qt::CaseInsensitive) == 0 || value == QStringLiteral("0")) setBool(false); else ok = false; break; case Field::CPPTYPE_INT32: { const qlonglong number = value.toLongLong(&ok); if (ok) field->is_repeated() ? reflection->SetRepeatedInt32(message, field, index, static_cast(number)) : reflection->SetInt32(message, field, static_cast(number)); break; } case Field::CPPTYPE_INT64: { const qlonglong number = value.toLongLong(&ok); if (ok) field->is_repeated() ? reflection->SetRepeatedInt64(message, field, index, number) : reflection->SetInt64(message, field, number); break; } case Field::CPPTYPE_UINT32: { const qulonglong number = value.toULongLong(&ok); if (ok) field->is_repeated() ? reflection->SetRepeatedUInt32(message, field, index, static_cast(number)) : reflection->SetUInt32(message, field, static_cast(number)); break; } case Field::CPPTYPE_UINT64: { const qulonglong number = value.toULongLong(&ok); if (ok) field->is_repeated() ? reflection->SetRepeatedUInt64(message, field, index, number) : reflection->SetUInt64(message, field, number); break; } case Field::CPPTYPE_FLOAT: { const float number = value.toFloat(&ok); if (ok) field->is_repeated() ? reflection->SetRepeatedFloat(message, field, index, number) : reflection->SetFloat(message, field, number); break; } case Field::CPPTYPE_DOUBLE: { const double number = value.toDouble(&ok); if (ok) field->is_repeated() ? reflection->SetRepeatedDouble(message, field, index, number) : reflection->SetDouble(message, field, number); break; } case Field::CPPTYPE_STRING: if (field->is_repeated()) reflection->SetRepeatedString(message, field, index, text.toUtf8().toStdString()); else reflection->SetString(message, field, text.toUtf8().toStdString()); break; case Field::CPPTYPE_ENUM: { const auto *enumValue = field->enum_type()->FindValueByName(value.toStdString()); if (!enumValue) { int number = value.toInt(&ok); enumValue = field->enum_type()->FindValueByNumber(number); } if (!enumValue) ok = false; else if (field->is_repeated()) reflection->SetRepeatedEnum(message, field, index, enumValue); else reflection->SetEnum(message, field, enumValue); break; } case Field::CPPTYPE_MESSAGE: ok = false; break; } return ok; } bool ConfigEditorWidget::syncFieldTreeToText() { if (!m_dynamicMessage) return false; google::protobuf::TextFormat::Printer printer; std::string text; if (!printer.PrintToString(*m_dynamicMessage, &text)) return false; m_loading = true; { const QSignalBlocker blocker(m_textEditor); m_textEditor->setPlainText(QString::fromStdString(text)); m_textEditor->document()->setModified(true); } m_loading = false; return true; } void ConfigEditorWidget::handleFieldItemChanged(QTreeWidgetItem *item, int column) { if (m_loading || column != 1 || !item || !item->data(1, kFieldRole).isValid()) return; const QString previous = item->data(1, kPreviousValueRole).toString(); if (!applyFieldValue(item, item->text(1)) || !syncFieldTreeToText()) { const QSignalBlocker blocker(m_fieldTree); item->setText(1, previous); setStatus(QStringLiteral("字段值格式无效,修改未应用"), true); return; } item->setData(1, kPreviousValueRole, item->text(1)); m_dirty = true; updateActions(); m_parseTimer->start(); } void ConfigEditorWidget::rebuildEnableTable() { rebuildFieldTree(); m_enableTable->setRowCount(0); const ManagerKind kind = managerKindForPath(m_currentPath); m_editorTabs->setTabEnabled(2, kind != ManagerKind::None); if (kind == ManagerKind::None) return; const std::string text = m_textEditor->toPlainText().toStdString(); QStringList ids; QStringList types; QStringList configFiles; QList enabledValues; bool parsed = false; if (kind == ManagerKind::Device) { cmvr::config::DeviceManagerRootConfig root; parsed = google::protobuf::TextFormat::ParseFromString(text, &root); if (parsed) { for (const auto &entry : root.device_manager().devices()) { ids.append(QString::fromStdString(entry.id())); types.append(QString::fromStdString( cmvr::config::DeviceConfigEntry::DeviceType_Name(entry.type()))); configFiles.append(QString::fromStdString(entry.config_file())); enabledValues.append(entry.enable()); } } } else { cmvr::config::TaskManagerRootConfig root; parsed = google::protobuf::TextFormat::ParseFromString(text, &root); if (parsed) { for (const auto &entry : root.task_manager().tasks()) { ids.append(QString::fromStdString(entry.id())); types.append(QString::fromStdString( cmvr::config::TaskConfigEntry::TaskType_Name(entry.type()))); configFiles.append(QString::fromStdString(entry.config_file())); enabledValues.append(entry.enable()); } } } if (!parsed) { setStatus(QStringLiteral("原始配置存在语法错误,启用项暂不可编辑"), true); return; } m_enableTable->setRowCount(ids.size()); for (int row = 0; row < ids.size(); ++row) { m_enableTable->setItem(row, 0, readOnlyItem(ids.at(row))); m_enableTable->setItem(row, 1, readOnlyItem(types.at(row))); m_enableTable->setItem(row, 2, readOnlyItem(configFiles.at(row))); auto *container = new QWidget(m_enableTable); auto *switchLayout = new QHBoxLayout(container); switchLayout->setContentsMargins(8, 0, 8, 0); switchLayout->setAlignment(Qt::AlignCenter); auto *toggle = new QCheckBox(container); toggle->setObjectName(QStringLiteral("configEnableSwitch")); toggle->setChecked(enabledValues.at(row)); toggle->setToolTip(enabledValues.at(row) ? QStringLiteral("点击停用") : QStringLiteral("点击启用")); switchLayout->addWidget(toggle); m_enableTable->setCellWidget(row, 3, container); connect(toggle, &QCheckBox::toggled, this, [this, row, toggle](bool checked) { if (!replaceEnableValue(row, checked)) { const QSignalBlocker blocker(toggle); toggle->setChecked(!checked); setStatus(QStringLiteral("无法定位对应的 enable 字段"), true); } }); } } bool ConfigEditorWidget::confirmDiscardChanges() { if (!m_dirty) return true; const auto answer = QMessageBox::question( this, QStringLiteral("未保存的修改"), QStringLiteral("当前配置尚未保存,确定放弃修改吗?"), QMessageBox::Discard | QMessageBox::Cancel, QMessageBox::Cancel); return answer == QMessageBox::Discard; } void ConfigEditorWidget::setBusy(bool busy) { m_busy = busy; updateActions(); } void ConfigEditorWidget::setStatus(const QString &text, bool error) { m_statusLabel->setText(text); m_statusLabel->setStyleSheet(error ? QStringLiteral("color:#b42318;") : QString()); } void ConfigEditorWidget::clearCurrentFile() { m_loading = true; m_currentPath.clear(); m_revision.clear(); m_dirty = false; m_textEditor->clear(); m_messageType.clear(); m_dynamicFactory.reset(); m_dynamicMessage.reset(); m_fieldTree->clear(); m_loading = false; m_pathLabel->setText(QStringLiteral("未选择配置文件")); m_typeLabel->setText(QStringLiteral("--")); m_enableTable->setRowCount(0); m_editorTabs->setTabEnabled(2, false); updateActions(); } void ConfigEditorWidget::updateActions() { const bool hasFile = !m_currentPath.isEmpty(); m_refreshButton->setEnabled(m_connected && !m_busy); m_fileList->setEnabled(m_connected && !m_busy); m_textEditor->setEnabled(hasFile && m_connected && !m_busy); m_fieldTree->setEnabled(hasFile && m_connected && !m_busy); m_enableTable->setEnabled(hasFile && m_connected && !m_busy); m_reloadButton->setEnabled(hasFile && m_connected && !m_busy); m_validateButton->setEnabled(hasFile && m_connected && !m_busy); m_saveButton->setEnabled(hasFile && m_connected && m_dirty && !m_busy); m_connectButton->setEnabled(!m_busy); } bool ConfigEditorWidget::replaceEnableValue(int index, bool enabled) { QString text = m_textEditor->toPlainText(); const QRegularExpression expression(QStringLiteral("\\benable\\s*:\\s*(true|false)")); auto matches = expression.globalMatch(text); QRegularExpressionMatch target; int current = 0; for (; current <= index && matches.hasNext(); ++current) target = matches.next(); if (!target.hasMatch() || current <= index) return false; QTextCursor cursor(m_textEditor->document()); cursor.setPosition(target.capturedStart(1)); cursor.setPosition(target.capturedEnd(1), QTextCursor::KeepAnchor); m_loading = true; cursor.insertText(enabled ? QStringLiteral("true") : QStringLiteral("false")); m_loading = false; m_dirty = true; m_textEditor->document()->setModified(true); updateActions(); return true; } ConfigEditorWidget::ManagerKind ConfigEditorWidget::managerKindForPath( const QString &path) const { const QString normalized = path.toLower(); if (normalized == QStringLiteral("manager/device_manager.pb.txt")) return ManagerKind::Device; if (normalized == QStringLiteral("manager/task_manager.pb.txt")) return ManagerKind::Task; return ManagerKind::None; }