CmvrControlStation/src/configeditorwidget.cpp

2602 lines
117 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include "configeditorwidget.h"
#include "robotclient.h"
#include "uiicons.h"
#include "cmvr/config/device_manager_config/device_manager_config.pb.h"
#include "cmvr/config/task_manager_config/task_manager_config.pb.h"
#include <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>
#include <QDateTime>
#include <QDir>
#include <QFormLayout>
#include <QFrame>
#include <QFontDatabase>
#include <QGridLayout>
#include <QHBoxLayout>
#include <QHeaderView>
#include <QLabel>
#include <QLineEdit>
#include <QListWidget>
#include <QMessageBox>
#include <QMouseEvent>
#include <QPlainTextEdit>
#include <QPushButton>
#include <QRegularExpression>
#include <QScrollArea>
#include <QSettings>
#include <QSet>
#include <QSignalBlocker>
#include <QSpinBox>
#include <QSplitter>
#include <QTabWidget>
#include <QTableWidget>
#include <QTimer>
#include <QToolButton>
#include <QTextCursor>
#include <QTreeWidget>
#include <QTreeWidgetItem>
#include <QUuid>
#include <QVBoxLayout>
#include <algorithm>
#include <cstdint>
#include <functional>
#include <utility>
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);
}
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);
auto *connectionBar = new QWidget(configPage);
connectionBar->setObjectName(QStringLiteral("panel"));
auto *connectionLayout = new QHBoxLayout(connectionBar);
connectionLayout->setContentsMargins(16, 12, 16, 12);
connectionLayout->setSpacing(8);
auto *hostLabel = new QLabel(QStringLiteral("配置服务"), connectionBar);
hostLabel->setObjectName(QStringLiteral("sectionTitle"));
connectionLayout->addWidget(hostLabel);
m_hostEdit = new QLineEdit(connectionBar);
m_hostEdit->setPlaceholderText(QStringLiteral("设备地址"));
m_hostEdit->setMinimumWidth(180);
m_portSpin = new QSpinBox(connectionBar);
m_portSpin->setRange(1, 65535);
m_portSpin->setButtonSymbols(QAbstractSpinBox::NoButtons);
m_portSpin->setFixedWidth(76);
QSettings settings;
m_hostEdit->setText(settings.value(QStringLiteral("configuration/host"),
QStringLiteral("192.168.28.248")).toString());
const int savedConfigurationPort = settings.value(
QStringLiteral("configuration/port"), 50053).toInt();
m_portSpin->setValue(savedConfigurationPort == 50052 ? 50053 : savedConfigurationPort);
connectionLayout->addWidget(m_hostEdit, 1);
connectionLayout->addWidget(m_portSpin);
m_connectButton = new QPushButton(QStringLiteral("连接"), connectionBar);
m_connectButton->setObjectName(QStringLiteral("primaryButton"));
m_refreshButton = new QPushButton(QStringLiteral("刷新"), connectionBar);
m_refreshButton->setEnabled(false);
connectionLayout->addWidget(m_connectButton);
connectionLayout->addWidget(m_refreshButton);
configLayout->addWidget(connectionBar);
auto *splitter = new QSplitter(Qt::Horizontal, configPage);
splitter->setChildrenCollapsible(false);
m_fileList = new QListWidget(splitter);
m_fileList->setMinimumWidth(260);
m_fileList->setMaximumWidth(380);
m_fileList->setAlternatingRowColors(true);
auto *editorPanel = new QWidget(splitter);
auto *editorLayout = new QVBoxLayout(editorPanel);
editorLayout->setContentsMargins(12, 0, 0, 0);
editorLayout->setSpacing(8);
m_rootLabel = new QLabel(QStringLiteral("远端根目录:--"), editorPanel);
m_rootLabel->setObjectName(QStringLiteral("mutedLabel"));
m_rootLabel->setTextInteractionFlags(Qt::TextSelectableByMouse);
m_pathLabel = new QLabel(QStringLiteral("未选择配置文件"), editorPanel);
m_pathLabel->setObjectName(QStringLiteral("sectionTitle"));
m_pathLabel->setTextInteractionFlags(Qt::TextSelectableByMouse);
m_typeLabel = new QLabel(QStringLiteral("--"), editorPanel);
m_typeLabel->setObjectName(QStringLiteral("mutedLabel"));
editorLayout->addWidget(m_rootLabel);
editorLayout->addWidget(m_pathLabel);
editorLayout->addWidget(m_typeLabel);
m_editorTabs = new QTabWidget(editorPanel);
m_textEditor = new QPlainTextEdit(m_editorTabs);
m_textEditor->setLineWrapMode(QPlainTextEdit::NoWrap);
m_textEditor->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont));
m_textEditor->setPlaceholderText(QStringLiteral("选择左侧配置文件后在此编辑"));
m_textEditor->setEnabled(false);
m_editorTabs->addTab(m_textEditor, QStringLiteral("原始配置"));
m_fieldTree = new QTreeWidget(m_editorTabs);
m_fieldTree->setColumnCount(3);
m_fieldTree->setHeaderLabels({QStringLiteral("字段名"), QStringLiteral("值"), QStringLiteral("类型")});
m_fieldTree->setAlternatingRowColors(true);
m_fieldTree->setRootIsDecorated(true);
m_fieldTree->setUniformRowHeights(true);
m_fieldTree->header()->setSectionResizeMode(0, QHeaderView::ResizeToContents);
m_fieldTree->header()->setSectionResizeMode(1, QHeaderView::Stretch);
m_fieldTree->header()->setSectionResizeMode(2, QHeaderView::ResizeToContents);
m_fieldTree->setEditTriggers(QAbstractItemView::DoubleClicked |
QAbstractItemView::EditKeyPressed |
QAbstractItemView::SelectedClicked);
m_editorTabs->addTab(m_fieldTree, QStringLiteral("字段编辑"));
m_enableTable = new QTableWidget(m_editorTabs);
m_enableTable->setColumnCount(4);
m_enableTable->setHorizontalHeaderLabels(
{QStringLiteral("ID"), QStringLiteral("类型"), QStringLiteral("配置文件"),
QStringLiteral("启用")});
m_enableTable->horizontalHeader()->setSectionResizeMode(0, QHeaderView::ResizeToContents);
m_enableTable->horizontalHeader()->setSectionResizeMode(1, QHeaderView::ResizeToContents);
m_enableTable->horizontalHeader()->setSectionResizeMode(2, QHeaderView::Stretch);
m_enableTable->horizontalHeader()->setSectionResizeMode(3, QHeaderView::ResizeToContents);
m_enableTable->verticalHeader()->setVisible(false);
m_enableTable->setSelectionBehavior(QAbstractItemView::SelectRows);
m_enableTable->setEditTriggers(QAbstractItemView::NoEditTriggers);
m_editorTabs->addTab(m_enableTable, QStringLiteral("启用项"));
m_editorTabs->setTabEnabled(2, false);
editorLayout->addWidget(m_editorTabs, 1);
auto *actionLayout = new QHBoxLayout;
m_statusLabel = new QLabel(QStringLiteral("未连接配置服务"), editorPanel);
m_statusLabel->setObjectName(QStringLiteral("mutedLabel"));
m_statusLabel->setWordWrap(true);
actionLayout->addWidget(m_statusLabel, 1);
m_reloadButton = new QPushButton(QStringLiteral("重新加载"), editorPanel);
m_validateButton = new QPushButton(QStringLiteral("校验"), editorPanel);
m_saveButton = new QPushButton(QStringLiteral("保存到设备"), editorPanel);
m_saveButton->setObjectName(QStringLiteral("primaryButton"));
actionLayout->addWidget(m_reloadButton);
actionLayout->addWidget(m_validateButton);
actionLayout->addWidget(m_saveButton);
editorLayout->addLayout(actionLayout);
splitter->addWidget(m_fileList);
splitter->addWidget(editorPanel);
splitter->setStretchFactor(0, 0);
splitter->setStretchFactor(1, 1);
configLayout->addWidget(splitter, 1);
pageTabs->addTab(configPage, QStringLiteral("配置管理"));
layout->addWidget(pageTabs, 1);
m_parseTimer = new QTimer(this);
m_parseTimer->setSingleShot(true);
m_parseTimer->setInterval(250);
connect(m_parseTimer, &QTimer::timeout, this, &ConfigEditorWidget::rebuildEnableTable);
connect(m_connectButton, &QPushButton::clicked, this, &ConfigEditorWidget::toggleConnection);
connect(m_refreshButton, &QPushButton::clicked, this, &ConfigEditorWidget::refreshFiles);
connect(m_fileList, &QListWidget::currentItemChanged,
this, &ConfigEditorWidget::selectFile);
connect(m_reloadButton, &QPushButton::clicked, this, &ConfigEditorWidget::reloadCurrentFile);
connect(m_validateButton, &QPushButton::clicked, this, &ConfigEditorWidget::validateCurrentFile);
connect(m_saveButton, &QPushButton::clicked, this, &ConfigEditorWidget::saveCurrentFile);
connect(m_textEditor, &QPlainTextEdit::textChanged,
this, &ConfigEditorWidget::handleEditorChanged);
connect(m_fieldTree, &QTreeWidget::itemChanged,
this, &ConfigEditorWidget::handleFieldItemChanged);
connect(m_deviceRefreshButton, &QPushButton::clicked,
m_client, &RobotClient::requestSystemRefresh);
connect(m_deviceSearchEdit, &QLineEdit::textChanged,
this, &ConfigEditorWidget::rebuildDeviceCards);
connect(m_deviceEditButton, &QPushButton::clicked,
this, &ConfigEditorWidget::toggleDeviceEditing);
connect(m_client, &RobotClient::connectionChanged,
this, &ConfigEditorWidget::handleEdgeConnectionChanged);
connect(m_client, &RobotClient::systemStatusReceived,
this, &ConfigEditorWidget::handleSystemStatus);
connect(m_client, &RobotClient::systemInfoReceived,
this, &ConfigEditorWidget::handleSystemInfo);
connect(m_client, &RobotClient::configurationConnectionChanged,
this, &ConfigEditorWidget::handleConnectionChanged);
connect(m_client, &RobotClient::configurationFilesReceived,
this, &ConfigEditorWidget::handleFilesReceived);
connect(m_client, &RobotClient::configurationFileReceived,
this, &ConfigEditorWidget::handleFileReceived);
connect(m_client, &RobotClient::configurationValidationFinished,
this, &ConfigEditorWidget::handleValidationFinished);
connect(m_client, &RobotClient::configurationSaveFinished,
this, &ConfigEditorWidget::handleSaveFinished);
rebuildDeviceCards();
updateActions();
}
void ConfigEditorWidget::handleEdgeConnectionChanged(bool connected, int latencyMs,
const QString &detail)
{
m_edgeConnected = connected;
m_demoMode = connected && detail == QStringLiteral("演示数据源");
if (connected && !m_demoMode) {
const QString endpoint = detail.trimmed();
const int separator = endpoint.lastIndexOf(QLatin1Char(':'));
m_edgeHost = separator > 0 ? endpoint.left(separator) : endpoint;
if (!m_edgeHost.isEmpty())
m_hostEdit->setText(m_edgeHost);
}
m_deviceRefreshButton->setEnabled(connected);
if (!m_deviceEditMode && !m_overviewLoading && !m_deviceEditRequested)
m_deviceEditButton->setEnabled(connected);
m_edgeConnectionChip->setProperty("connected", connected);
m_edgeConnectionChip->setText(connected
? QStringLiteral("● 边缘端在线 · %1 ms").arg(latencyMs)
: QStringLiteral("● 边缘端未连接"));
m_edgeConnectionChip->setToolTip(detail);
m_edgeConnectionChip->style()->unpolish(m_edgeConnectionChip);
m_edgeConnectionChip->style()->polish(m_edgeConnectionChip);
if (!connected) {
if (m_deviceEditMode)
cancelDeviceEditing();
m_devices.clear();
m_onlineDeviceValue->setText(QStringLiteral("--"));
m_deviceTypeValue->setText(QStringLiteral("--"));
m_lastSyncValue->setText(QStringLiteral("--:--:--"));
m_edgeSystemLabel->setText(QStringLiteral("等待获取边缘端系统信息"));
rebuildDeviceCards();
return;
}
m_client->requestSystemRefresh();
}
void ConfigEditorWidget::handleSystemStatus(double cpu, double memory, double disk,
const QVariantList &devices)
{
Q_UNUSED(cpu)
Q_UNUSED(memory)
Q_UNUSED(disk)
m_devices = devices;
std::sort(m_devices.begin(), m_devices.end(), [](const QVariant &left,
const QVariant &right) {
const QVariantMap leftDevice = left.toMap();
const QVariantMap rightDevice = right.toMap();
const int leftType = leftDevice.value(QStringLiteral("type"), 20).toInt();
const int rightType = rightDevice.value(QStringLiteral("type"), 20).toInt();
if (leftType != rightType)
return leftType < rightType;
return leftDevice.value(QStringLiteral("deviceId")).toString()
.localeAwareCompare(rightDevice.value(QStringLiteral("deviceId")).toString()) < 0;
});
QSet<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 : qAsConst(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));
}
void ConfigEditorWidget::toggleConnection()
{
if (m_connected) {
if (!confirmDiscardChanges())
return;
m_client->disconnectConfigurationServer();
return;
}
if (m_hostEdit->text().trimmed().isEmpty()) {
setStatus(QStringLiteral("设备地址不能为空"), true);
return;
}
QSettings settings;
settings.setValue(QStringLiteral("configuration/host"), m_hostEdit->text().trimmed());
settings.setValue(QStringLiteral("configuration/port"), m_portSpin->value());
setBusy(true);
setStatus(QStringLiteral("正在连接配置服务..."));
m_client->connectConfigurationServer(m_hostEdit->text(), m_portSpin->value());
}
void ConfigEditorWidget::refreshFiles()
{
if (!m_connected || !confirmDiscardChanges())
return;
setBusy(true);
setStatus(QStringLiteral("正在读取远端配置列表..."));
m_client->requestConfigurationFiles();
}
void ConfigEditorWidget::selectFile(QListWidgetItem *current, QListWidgetItem *previous)
{
if (!current)
return;
const QString nextPath = current->data(kPathRole).toString();
if (nextPath == m_currentPath)
return;
if (!confirmDiscardChanges()) {
const QSignalBlocker blocker(m_fileList);
m_fileList->setCurrentItem(previous);
return;
}
setBusy(true);
setStatus(QStringLiteral("正在读取 %1...").arg(nextPath));
m_client->readConfigurationFile(nextPath);
}
void ConfigEditorWidget::reloadCurrentFile()
{
if (m_currentPath.isEmpty() || !confirmDiscardChanges())
return;
setBusy(true);
m_client->readConfigurationFile(m_currentPath);
}
void ConfigEditorWidget::validateCurrentFile()
{
if (m_currentPath.isEmpty())
return;
setBusy(true);
setStatus(QStringLiteral("正在校验 %1...").arg(m_currentPath));
m_client->validateConfigurationFile(m_currentPath, m_textEditor->toPlainText());
}
void ConfigEditorWidget::saveCurrentFile()
{
if (m_currentPath.isEmpty() || m_revision.isEmpty())
return;
setBusy(true);
setStatus(QStringLiteral("正在保存 %1...").arg(m_currentPath));
m_client->saveConfigurationFile(m_currentPath, m_textEditor->toPlainText(), m_revision);
}
void ConfigEditorWidget::handleConnectionChanged(bool connected, const QString &detail)
{
m_connected = connected;
setBusy(false);
m_hostEdit->setEnabled(!connected);
m_portSpin->setEnabled(!connected);
m_connectButton->setText(connected ? QStringLiteral("断开") : QStringLiteral("连接"));
setStatus(detail, !connected && detail != QStringLiteral("配置服务已断开"));
if (connected && m_deviceEditRequested) {
beginDeviceConfigurationLoad();
} else if (connected) {
refreshFiles();
}
else {
if (m_deviceEditRequested || m_overviewLoading) {
m_deviceEditRequested = false;
m_overviewLoading = false;
m_deviceEditButton->setEnabled(m_edgeConnected);
m_deviceEditButton->setText(QStringLiteral("修改"));
}
m_fileList->clear();
m_rootLabel->setText(QStringLiteral("远端根目录:--"));
clearCurrentFile();
}
updateActions();
}
void ConfigEditorWidget::handleFilesReceived(const QString &configRoot,
const QVariantList &files,
bool success, const QString &detail)
{
setBusy(false);
if (!success) {
if (m_overviewLoading) {
m_overviewLoading = false;
m_deviceEditButton->setEnabled(true);
m_deviceEditButton->setText(QStringLiteral("修改"));
}
setStatus(detail.isEmpty() ? QStringLiteral("读取配置列表失败") : detail, true);
return;
}
if (m_overviewLoading) {
Q_UNUSED(configRoot)
m_configurationFiles = files;
m_overviewReadQueue.clear();
m_managerPath.clear();
const QSet<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;
}
const QSignalBlocker blocker(m_fileList);
m_fileList->clear();
clearCurrentFile();
for (const QVariant &value : files) {
const QVariantMap file = value.toMap();
const QString path = file.value(QStringLiteral("relativePath")).toString();
auto *item = new QListWidgetItem(path, m_fileList);
item->setData(kPathRole, path);
item->setToolTip(QStringLiteral("%1\n%2 bytes\n%3")
.arg(file.value(QStringLiteral("messageType")).toString())
.arg(file.value(QStringLiteral("sizeBytes")).toULongLong())
.arg(QDateTime::fromMSecsSinceEpoch(
file.value(QStringLiteral("modifiedUnixMs")).toLongLong()).toString(Qt::ISODate)));
}
m_rootLabel->setText(QStringLiteral("远端根目录:%1").arg(configRoot));
setStatus(QStringLiteral("已读取 %1 个配置文件").arg(files.size()));
updateActions();
}
void ConfigEditorWidget::handleFileReceived(const QString &relativePath,
const QString &content,
const QString &revision,
const QString &messageType,
bool success, const QString &detail)
{
setBusy(false);
if (m_overviewLoading && relativePath == m_overviewCurrentRead) {
if (!success) {
m_overviewLoading = false;
m_deviceEditButton->setEnabled(true);
m_deviceEditButton->setText(QStringLiteral("修改"));
QMessageBox::warning(this, QStringLiteral("读取设备配置"),
detail.isEmpty() ? QStringLiteral("读取 %1 失败").arg(relativePath)
: detail);
return;
}
m_overviewContents.insert(relativePath, content);
m_overviewRevisions.insert(relativePath, revision);
m_overviewMessageTypes.insert(relativePath, messageType);
if (relativePath == m_managerPath) {
m_managerContent = content;
m_managerRevision = revision;
m_managerMessageType = messageType;
}
readNextOverviewConfiguration();
return;
}
if (!success) {
setStatus(detail.isEmpty() ? QStringLiteral("读取配置文件失败") : detail, true);
return;
}
m_loading = true;
m_currentPath = relativePath;
m_revision = revision;
m_messageType = messageType;
m_textEditor->setPlainText(content);
m_textEditor->document()->setModified(false);
m_loading = false;
m_dirty = false;
m_pathLabel->setText(relativePath);
m_typeLabel->setText(QStringLiteral("消息类型:%1 版本:%2")
.arg(messageType, revision));
rebuildFieldTree();
rebuildEnableTable();
setStatus(QStringLiteral("配置已加载"));
updateActions();
}
void ConfigEditorWidget::handleValidationFinished(const QString &relativePath,
bool success,
const QString &detail)
{
if (relativePath != m_currentPath)
return;
setBusy(false);
setStatus(detail.isEmpty() ? (success ? QStringLiteral("配置有效")
: QStringLiteral("配置无效")) : detail,
!success);
}
void ConfigEditorWidget::handleSaveFinished(const QString &relativePath,
const QString &revision,
bool success,
const QString &detail)
{
if (m_overviewSaving && relativePath == m_overviewCurrentSave) {
if (!success) {
m_overviewSaving = false;
m_deviceEditButton->setEnabled(true);
m_deviceEditButton->setText(QStringLiteral("完成"));
QMessageBox::critical(
this, QStringLiteral("保存设备配置失败"),
QStringLiteral("%1\n%2\n\n已成功保存的文件不会自动回滚,请刷新后再试。")
.arg(relativePath, detail.isEmpty() ? QStringLiteral("远端拒绝保存") : detail));
return;
}
m_overviewRevisions.insert(relativePath, revision);
saveNextOverviewConfiguration();
return;
}
if (relativePath != m_currentPath)
return;
setBusy(false);
if (!success) {
setStatus(detail.isEmpty() ? QStringLiteral("保存失败") : detail, true);
return;
}
m_revision = revision;
m_dirty = false;
m_textEditor->document()->setModified(false);
m_typeLabel->setText(m_typeLabel->text().section(QStringLiteral(" 版本:"), 0, 0)
+ QStringLiteral(" 版本:") + revision);
setStatus(QStringLiteral("已保存到远端项目配置文件"));
updateActions();
}
void ConfigEditorWidget::handleEditorChanged()
{
if (m_loading || m_currentPath.isEmpty())
return;
m_dirty = true;
m_parseTimer->start();
updateActions();
}
bool ConfigEditorWidget::parseFieldDocument()
{
m_dynamicMessage.reset();
m_dynamicFactory.reset();
if (m_messageType.isEmpty() || m_textEditor->toPlainText().trimmed().isEmpty())
return false;
const auto *descriptor = google::protobuf::DescriptorPool::generated_pool()
->FindMessageTypeByName(m_messageType.toStdString());
if (!descriptor)
return false;
m_dynamicFactory = std::make_unique<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;
}