CmvrControlStation/src/configeditorwidget.cpp

2602 lines
117 KiB
C++
Raw Normal View History

2026-09-18 16:22:20 +08:00
#include "configeditorwidget.h"
#include "robotclient.h"
#include "uiicons.h"
2026-09-18 16:22:20 +08:00
#include "cmvr/config/device_manager_config/device_manager_config.pb.h"
#include "cmvr/config/task_manager_config/task_manager_config.pb.h"
#include <google/protobuf/descriptor.h>
#include <google/protobuf/dynamic_message.h>
#include <google/protobuf/message.h>
#include <google/protobuf/reflection.h>
#include <google/protobuf/text_format.h>
#include <QCheckBox>
#include <QComboBox>
2026-09-18 16:22:20 +08:00
#include <QDateTime>
#include <QDir>
2026-09-18 16:22:20 +08:00
#include <QFormLayout>
#include <QFrame>
2026-09-18 16:22:20 +08:00
#include <QFontDatabase>
#include <QGridLayout>
2026-09-18 16:22:20 +08:00
#include <QHBoxLayout>
#include <QHeaderView>
#include <QLabel>
#include <QLineEdit>
#include <QListWidget>
#include <QMessageBox>
#include <QMouseEvent>
2026-09-18 16:22:20 +08:00
#include <QPlainTextEdit>
#include <QPushButton>
#include <QRegularExpression>
#include <QScrollArea>
2026-09-18 16:22:20 +08:00
#include <QSettings>
#include <QSet>
2026-09-18 16:22:20 +08:00
#include <QSignalBlocker>
#include <QSpinBox>
#include <QSplitter>
#include <QTabWidget>
#include <QTableWidget>
#include <QTimer>
#include <QToolButton>
2026-09-18 16:22:20 +08:00
#include <QTextCursor>
#include <QTreeWidget>
#include <QTreeWidgetItem>
#include <QUuid>
2026-09-18 16:22:20 +08:00
#include <QVBoxLayout>
#include <algorithm>
#include <cstdint>
#include <functional>
#include <utility>
2026-09-18 16:22:20 +08:00
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<void()> 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<google::protobuf::Message> 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<google::protobuf::Message> 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<QString> &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<int>(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);
}
2026-09-18 16:22:20 +08:00
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<quintptr>(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<qlonglong>(
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<qulonglong>(
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<qulonglong>(reinterpret_cast<quintptr>(message)));
item->setData(1, kFieldRole,
QVariant::fromValue<qulonglong>(reinterpret_cast<quintptr>(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 &note,
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);
2026-09-18 16:22:20 +08:00
auto *connectionBar = new QWidget(configPage);
2026-09-18 16:22:20 +08:00
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);
2026-09-18 16:22:20 +08:00
auto *splitter = new QSplitter(Qt::Horizontal, configPage);
2026-09-18 16:22:20 +08:00
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);
2026-09-18 16:22:20 +08:00
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);
2026-09-18 16:22:20 +08:00
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();
2026-09-18 16:22:20 +08:00
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<int> 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<QString> 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<QPair<QString, int>> 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<int>::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<int>::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<QString> 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<int>(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<QString> originalIds;
for (const QVariant &value : m_devices)
originalIds.insert(value.toMap().value(QStringLiteral("deviceId")).toString());
QSet<QString> 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<QString> 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<cmvr::config::DeviceConfigEntry::DeviceType>(
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 &parameterValue : 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));
}
2026-09-18 16:22:20 +08:00
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) {
2026-09-18 16:22:20 +08:00
refreshFiles();
}
2026-09-18 16:22:20 +08:00
else {
if (m_deviceEditRequested || m_overviewLoading) {
m_deviceEditRequested = false;
m_overviewLoading = false;
m_deviceEditButton->setEnabled(m_edgeConnected);
m_deviceEditButton->setText(QStringLiteral("修改"));
}
2026-09-18 16:22:20 +08:00
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("修改"));
}
2026-09-18 16:22:20 +08:00
setStatus(detail.isEmpty() ? QStringLiteral("读取配置列表失败") : detail, true);
return;
}
if (m_overviewLoading) {
Q_UNUSED(configRoot)
m_configurationFiles = files;
m_overviewReadQueue.clear();
m_managerPath.clear();
const QSet<QString> 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;
}
2026-09-18 16:22:20 +08:00
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;
}
2026-09-18 16:22:20 +08:00
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;
}
2026-09-18 16:22:20 +08:00
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<google::protobuf::DynamicMessageFactory>();
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<google::protobuf::Message *>(
pointerValue(item->data(1, kMessageRole)));
const auto *field = reinterpret_cast<const google::protobuf::FieldDescriptor *>(
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<int32_t>(number))
: reflection->SetInt32(message, field, static_cast<int32_t>(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<uint32_t>(number))
: reflection->SetUInt32(message, field, static_cast<uint32_t>(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<bool> 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;
}