2602 lines
117 KiB
C++
2602 lines
117 KiB
C++
#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 ¬e,
|
||
const QString &accent,
|
||
QLabel **valueLabel) {
|
||
auto *card = new QFrame(overviewPage);
|
||
card->setObjectName(QStringLiteral("deviceMetricCard"));
|
||
card->setProperty("accent", accent);
|
||
auto *cardLayout = new QVBoxLayout(card);
|
||
cardLayout->setContentsMargins(16, 13, 16, 13);
|
||
cardLayout->setSpacing(3);
|
||
auto *labelWidget = new QLabel(label, card);
|
||
labelWidget->setObjectName(QStringLiteral("deviceMetricLabel"));
|
||
auto *valueWidget = new QLabel(value, card);
|
||
valueWidget->setObjectName(QStringLiteral("deviceMetricValue"));
|
||
auto *noteWidget = new QLabel(note, card);
|
||
noteWidget->setObjectName(QStringLiteral("deviceMetricNote"));
|
||
cardLayout->addWidget(labelWidget);
|
||
cardLayout->addWidget(valueWidget);
|
||
cardLayout->addWidget(noteWidget);
|
||
metricsLayout->addWidget(card, 1);
|
||
*valueLabel = valueWidget;
|
||
};
|
||
addMetricCard(QStringLiteral("在线设备"), QStringLiteral("--"),
|
||
QStringLiteral("GetSystemStatus 实时返回"), QStringLiteral("green"),
|
||
&m_onlineDeviceValue);
|
||
addMetricCard(QStringLiteral("设备类型"), QStringLiteral("--"),
|
||
QStringLiteral("按 DeviceType 自动归类"), QStringLiteral("blue"),
|
||
&m_deviceTypeValue);
|
||
addMetricCard(QStringLiteral("最近同步"), QStringLiteral("--:--:--"),
|
||
QStringLiteral("在线状态每秒自动更新"), QStringLiteral("orange"),
|
||
&m_lastSyncValue);
|
||
overviewLayout->addLayout(metricsLayout);
|
||
|
||
auto *deviceHeader = new QHBoxLayout;
|
||
deviceHeader->setContentsMargins(2, 4, 2, 0);
|
||
auto *deviceHeaderText = new QVBoxLayout;
|
||
deviceHeaderText->setSpacing(2);
|
||
m_deviceSectionTitle = new QLabel(QStringLiteral("在线设备"), overviewPage);
|
||
m_deviceSectionTitle->setObjectName(QStringLiteral("deviceSectionTitle"));
|
||
m_deviceSectionHint = new QLabel(
|
||
QStringLiteral("设备 ID 与类型由边缘端 DeviceManager 注册信息生成"), overviewPage);
|
||
m_deviceSectionHint->setObjectName(QStringLiteral("deviceSectionHint"));
|
||
deviceHeaderText->addWidget(m_deviceSectionTitle);
|
||
deviceHeaderText->addWidget(m_deviceSectionHint);
|
||
deviceHeader->addLayout(deviceHeaderText, 1);
|
||
m_deviceEditButton = new QPushButton(QStringLiteral("修改"), overviewPage);
|
||
m_deviceEditButton->setObjectName(QStringLiteral("deviceEditButton"));
|
||
m_deviceEditButton->setIcon(UiIcons::icon(QStringLiteral(":/icons/edit-3.svg")));
|
||
m_deviceEditButton->setEnabled(false);
|
||
deviceHeader->addWidget(m_deviceEditButton);
|
||
m_deviceSearchEdit = new QLineEdit(overviewPage);
|
||
m_deviceSearchEdit->setObjectName(QStringLiteral("deviceSearchEdit"));
|
||
m_deviceSearchEdit->setPlaceholderText(QStringLiteral("搜索设备 ID 或类型"));
|
||
m_deviceSearchEdit->setClearButtonEnabled(true);
|
||
m_deviceSearchEdit->setMinimumWidth(240);
|
||
deviceHeader->addWidget(m_deviceSearchEdit);
|
||
overviewLayout->addLayout(deviceHeader);
|
||
|
||
m_deviceGridHost = new QWidget(overviewPage);
|
||
m_deviceGridHost->setObjectName(QStringLiteral("deviceGridHost"));
|
||
m_deviceGrid = new QGridLayout(m_deviceGridHost);
|
||
m_deviceGrid->setContentsMargins(0, 0, 0, 0);
|
||
m_deviceGrid->setHorizontalSpacing(12);
|
||
m_deviceGrid->setVerticalSpacing(12);
|
||
overviewLayout->addWidget(m_deviceGridHost);
|
||
overviewLayout->addStretch(1);
|
||
overviewScroll->setWidget(overviewPage);
|
||
pageTabs->addTab(overviewScroll, QStringLiteral("设备总览"));
|
||
|
||
auto *configPage = new QWidget(pageTabs);
|
||
auto *configLayout = new QVBoxLayout(configPage);
|
||
configLayout->setContentsMargins(24, 18, 24, 18);
|
||
configLayout->setSpacing(12);
|
||
|
||
auto *connectionBar = new QWidget(configPage);
|
||
connectionBar->setObjectName(QStringLiteral("panel"));
|
||
auto *connectionLayout = new QHBoxLayout(connectionBar);
|
||
connectionLayout->setContentsMargins(16, 12, 16, 12);
|
||
connectionLayout->setSpacing(8);
|
||
auto *hostLabel = new QLabel(QStringLiteral("配置服务"), connectionBar);
|
||
hostLabel->setObjectName(QStringLiteral("sectionTitle"));
|
||
connectionLayout->addWidget(hostLabel);
|
||
m_hostEdit = new QLineEdit(connectionBar);
|
||
m_hostEdit->setPlaceholderText(QStringLiteral("设备地址"));
|
||
m_hostEdit->setMinimumWidth(180);
|
||
m_portSpin = new QSpinBox(connectionBar);
|
||
m_portSpin->setRange(1, 65535);
|
||
m_portSpin->setButtonSymbols(QAbstractSpinBox::NoButtons);
|
||
m_portSpin->setFixedWidth(76);
|
||
QSettings settings;
|
||
m_hostEdit->setText(settings.value(QStringLiteral("configuration/host"),
|
||
QStringLiteral("192.168.28.248")).toString());
|
||
const int savedConfigurationPort = settings.value(
|
||
QStringLiteral("configuration/port"), 50053).toInt();
|
||
m_portSpin->setValue(savedConfigurationPort == 50052 ? 50053 : savedConfigurationPort);
|
||
connectionLayout->addWidget(m_hostEdit, 1);
|
||
connectionLayout->addWidget(m_portSpin);
|
||
m_connectButton = new QPushButton(QStringLiteral("连接"), connectionBar);
|
||
m_connectButton->setObjectName(QStringLiteral("primaryButton"));
|
||
m_refreshButton = new QPushButton(QStringLiteral("刷新"), connectionBar);
|
||
m_refreshButton->setEnabled(false);
|
||
connectionLayout->addWidget(m_connectButton);
|
||
connectionLayout->addWidget(m_refreshButton);
|
||
configLayout->addWidget(connectionBar);
|
||
|
||
auto *splitter = new QSplitter(Qt::Horizontal, configPage);
|
||
splitter->setChildrenCollapsible(false);
|
||
m_fileList = new QListWidget(splitter);
|
||
m_fileList->setMinimumWidth(260);
|
||
m_fileList->setMaximumWidth(380);
|
||
m_fileList->setAlternatingRowColors(true);
|
||
|
||
auto *editorPanel = new QWidget(splitter);
|
||
auto *editorLayout = new QVBoxLayout(editorPanel);
|
||
editorLayout->setContentsMargins(12, 0, 0, 0);
|
||
editorLayout->setSpacing(8);
|
||
m_rootLabel = new QLabel(QStringLiteral("远端根目录:--"), editorPanel);
|
||
m_rootLabel->setObjectName(QStringLiteral("mutedLabel"));
|
||
m_rootLabel->setTextInteractionFlags(Qt::TextSelectableByMouse);
|
||
m_pathLabel = new QLabel(QStringLiteral("未选择配置文件"), editorPanel);
|
||
m_pathLabel->setObjectName(QStringLiteral("sectionTitle"));
|
||
m_pathLabel->setTextInteractionFlags(Qt::TextSelectableByMouse);
|
||
m_typeLabel = new QLabel(QStringLiteral("--"), editorPanel);
|
||
m_typeLabel->setObjectName(QStringLiteral("mutedLabel"));
|
||
editorLayout->addWidget(m_rootLabel);
|
||
editorLayout->addWidget(m_pathLabel);
|
||
editorLayout->addWidget(m_typeLabel);
|
||
|
||
m_editorTabs = new QTabWidget(editorPanel);
|
||
m_textEditor = new QPlainTextEdit(m_editorTabs);
|
||
m_textEditor->setLineWrapMode(QPlainTextEdit::NoWrap);
|
||
m_textEditor->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont));
|
||
m_textEditor->setPlaceholderText(QStringLiteral("选择左侧配置文件后在此编辑"));
|
||
m_textEditor->setEnabled(false);
|
||
m_editorTabs->addTab(m_textEditor, QStringLiteral("原始配置"));
|
||
m_fieldTree = new QTreeWidget(m_editorTabs);
|
||
m_fieldTree->setColumnCount(3);
|
||
m_fieldTree->setHeaderLabels({QStringLiteral("字段名"), QStringLiteral("值"), QStringLiteral("类型")});
|
||
m_fieldTree->setAlternatingRowColors(true);
|
||
m_fieldTree->setRootIsDecorated(true);
|
||
m_fieldTree->setUniformRowHeights(true);
|
||
m_fieldTree->header()->setSectionResizeMode(0, QHeaderView::ResizeToContents);
|
||
m_fieldTree->header()->setSectionResizeMode(1, QHeaderView::Stretch);
|
||
m_fieldTree->header()->setSectionResizeMode(2, QHeaderView::ResizeToContents);
|
||
m_fieldTree->setEditTriggers(QAbstractItemView::DoubleClicked |
|
||
QAbstractItemView::EditKeyPressed |
|
||
QAbstractItemView::SelectedClicked);
|
||
m_editorTabs->addTab(m_fieldTree, QStringLiteral("字段编辑"));
|
||
m_enableTable = new QTableWidget(m_editorTabs);
|
||
m_enableTable->setColumnCount(4);
|
||
m_enableTable->setHorizontalHeaderLabels(
|
||
{QStringLiteral("ID"), QStringLiteral("类型"), QStringLiteral("配置文件"),
|
||
QStringLiteral("启用")});
|
||
m_enableTable->horizontalHeader()->setSectionResizeMode(0, QHeaderView::ResizeToContents);
|
||
m_enableTable->horizontalHeader()->setSectionResizeMode(1, QHeaderView::ResizeToContents);
|
||
m_enableTable->horizontalHeader()->setSectionResizeMode(2, QHeaderView::Stretch);
|
||
m_enableTable->horizontalHeader()->setSectionResizeMode(3, QHeaderView::ResizeToContents);
|
||
m_enableTable->verticalHeader()->setVisible(false);
|
||
m_enableTable->setSelectionBehavior(QAbstractItemView::SelectRows);
|
||
m_enableTable->setEditTriggers(QAbstractItemView::NoEditTriggers);
|
||
m_editorTabs->addTab(m_enableTable, QStringLiteral("启用项"));
|
||
m_editorTabs->setTabEnabled(2, false);
|
||
editorLayout->addWidget(m_editorTabs, 1);
|
||
|
||
auto *actionLayout = new QHBoxLayout;
|
||
m_statusLabel = new QLabel(QStringLiteral("未连接配置服务"), editorPanel);
|
||
m_statusLabel->setObjectName(QStringLiteral("mutedLabel"));
|
||
m_statusLabel->setWordWrap(true);
|
||
actionLayout->addWidget(m_statusLabel, 1);
|
||
m_reloadButton = new QPushButton(QStringLiteral("重新加载"), editorPanel);
|
||
m_validateButton = new QPushButton(QStringLiteral("校验"), editorPanel);
|
||
m_saveButton = new QPushButton(QStringLiteral("保存到设备"), editorPanel);
|
||
m_saveButton->setObjectName(QStringLiteral("primaryButton"));
|
||
actionLayout->addWidget(m_reloadButton);
|
||
actionLayout->addWidget(m_validateButton);
|
||
actionLayout->addWidget(m_saveButton);
|
||
editorLayout->addLayout(actionLayout);
|
||
splitter->addWidget(m_fileList);
|
||
splitter->addWidget(editorPanel);
|
||
splitter->setStretchFactor(0, 0);
|
||
splitter->setStretchFactor(1, 1);
|
||
configLayout->addWidget(splitter, 1);
|
||
pageTabs->addTab(configPage, QStringLiteral("配置管理"));
|
||
layout->addWidget(pageTabs, 1);
|
||
|
||
m_parseTimer = new QTimer(this);
|
||
m_parseTimer->setSingleShot(true);
|
||
m_parseTimer->setInterval(250);
|
||
connect(m_parseTimer, &QTimer::timeout, this, &ConfigEditorWidget::rebuildEnableTable);
|
||
connect(m_connectButton, &QPushButton::clicked, this, &ConfigEditorWidget::toggleConnection);
|
||
connect(m_refreshButton, &QPushButton::clicked, this, &ConfigEditorWidget::refreshFiles);
|
||
connect(m_fileList, &QListWidget::currentItemChanged,
|
||
this, &ConfigEditorWidget::selectFile);
|
||
connect(m_reloadButton, &QPushButton::clicked, this, &ConfigEditorWidget::reloadCurrentFile);
|
||
connect(m_validateButton, &QPushButton::clicked, this, &ConfigEditorWidget::validateCurrentFile);
|
||
connect(m_saveButton, &QPushButton::clicked, this, &ConfigEditorWidget::saveCurrentFile);
|
||
connect(m_textEditor, &QPlainTextEdit::textChanged,
|
||
this, &ConfigEditorWidget::handleEditorChanged);
|
||
connect(m_fieldTree, &QTreeWidget::itemChanged,
|
||
this, &ConfigEditorWidget::handleFieldItemChanged);
|
||
connect(m_deviceRefreshButton, &QPushButton::clicked,
|
||
m_client, &RobotClient::requestSystemRefresh);
|
||
connect(m_deviceSearchEdit, &QLineEdit::textChanged,
|
||
this, &ConfigEditorWidget::rebuildDeviceCards);
|
||
connect(m_deviceEditButton, &QPushButton::clicked,
|
||
this, &ConfigEditorWidget::toggleDeviceEditing);
|
||
connect(m_client, &RobotClient::connectionChanged,
|
||
this, &ConfigEditorWidget::handleEdgeConnectionChanged);
|
||
connect(m_client, &RobotClient::systemStatusReceived,
|
||
this, &ConfigEditorWidget::handleSystemStatus);
|
||
connect(m_client, &RobotClient::systemInfoReceived,
|
||
this, &ConfigEditorWidget::handleSystemInfo);
|
||
connect(m_client, &RobotClient::configurationConnectionChanged,
|
||
this, &ConfigEditorWidget::handleConnectionChanged);
|
||
connect(m_client, &RobotClient::configurationFilesReceived,
|
||
this, &ConfigEditorWidget::handleFilesReceived);
|
||
connect(m_client, &RobotClient::configurationFileReceived,
|
||
this, &ConfigEditorWidget::handleFileReceived);
|
||
connect(m_client, &RobotClient::configurationValidationFinished,
|
||
this, &ConfigEditorWidget::handleValidationFinished);
|
||
connect(m_client, &RobotClient::configurationSaveFinished,
|
||
this, &ConfigEditorWidget::handleSaveFinished);
|
||
rebuildDeviceCards();
|
||
updateActions();
|
||
}
|
||
|
||
void ConfigEditorWidget::handleEdgeConnectionChanged(bool connected, int latencyMs,
|
||
const QString &detail)
|
||
{
|
||
m_edgeConnected = connected;
|
||
m_demoMode = connected && detail == QStringLiteral("演示数据源");
|
||
if (connected && !m_demoMode) {
|
||
const QString endpoint = detail.trimmed();
|
||
const int separator = endpoint.lastIndexOf(QLatin1Char(':'));
|
||
m_edgeHost = separator > 0 ? endpoint.left(separator) : endpoint;
|
||
if (!m_edgeHost.isEmpty())
|
||
m_hostEdit->setText(m_edgeHost);
|
||
}
|
||
m_deviceRefreshButton->setEnabled(connected);
|
||
if (!m_deviceEditMode && !m_overviewLoading && !m_deviceEditRequested)
|
||
m_deviceEditButton->setEnabled(connected);
|
||
m_edgeConnectionChip->setProperty("connected", connected);
|
||
m_edgeConnectionChip->setText(connected
|
||
? QStringLiteral("● 边缘端在线 · %1 ms").arg(latencyMs)
|
||
: QStringLiteral("● 边缘端未连接"));
|
||
m_edgeConnectionChip->setToolTip(detail);
|
||
m_edgeConnectionChip->style()->unpolish(m_edgeConnectionChip);
|
||
m_edgeConnectionChip->style()->polish(m_edgeConnectionChip);
|
||
|
||
if (!connected) {
|
||
if (m_deviceEditMode)
|
||
cancelDeviceEditing();
|
||
m_devices.clear();
|
||
m_onlineDeviceValue->setText(QStringLiteral("--"));
|
||
m_deviceTypeValue->setText(QStringLiteral("--"));
|
||
m_lastSyncValue->setText(QStringLiteral("--:--:--"));
|
||
m_edgeSystemLabel->setText(QStringLiteral("等待获取边缘端系统信息"));
|
||
rebuildDeviceCards();
|
||
return;
|
||
}
|
||
m_client->requestSystemRefresh();
|
||
}
|
||
|
||
void ConfigEditorWidget::handleSystemStatus(double cpu, double memory, double disk,
|
||
const QVariantList &devices)
|
||
{
|
||
Q_UNUSED(cpu)
|
||
Q_UNUSED(memory)
|
||
Q_UNUSED(disk)
|
||
m_devices = devices;
|
||
std::sort(m_devices.begin(), m_devices.end(), [](const QVariant &left,
|
||
const QVariant &right) {
|
||
const QVariantMap leftDevice = left.toMap();
|
||
const QVariantMap rightDevice = right.toMap();
|
||
const int leftType = leftDevice.value(QStringLiteral("type"), 20).toInt();
|
||
const int rightType = rightDevice.value(QStringLiteral("type"), 20).toInt();
|
||
if (leftType != rightType)
|
||
return leftType < rightType;
|
||
return leftDevice.value(QStringLiteral("deviceId")).toString()
|
||
.localeAwareCompare(rightDevice.value(QStringLiteral("deviceId")).toString()) < 0;
|
||
});
|
||
QSet<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 ¶meterValue : parameters) {
|
||
const QVariantMap parameter = parameterValue.toMap();
|
||
const QString parameterPath = parameter.value(QStringLiteral("path")).toString();
|
||
const QString parameterText = parameter.value(QStringLiteral("value")).toString();
|
||
if (parameterText.isEmpty() && parameterPath.endsWith(QLatin1Char(']')))
|
||
continue;
|
||
if (!setParameterValue(entry, parameterPath, parameterText)) {
|
||
if (error)
|
||
*error = QStringLiteral("设备 %1 的参数 %2 格式不正确")
|
||
.arg(id, parameterPath);
|
||
return false;
|
||
}
|
||
}
|
||
}
|
||
const QString serialized = printMessage(*root);
|
||
if (serialized.isEmpty()) {
|
||
if (error)
|
||
*error = QStringLiteral("无法序列化 %1").arg(path);
|
||
return false;
|
||
}
|
||
m_overviewSaveContents.insert(path, serialized);
|
||
m_overviewSaveRevisions.insert(path, m_overviewRevisions.value(path));
|
||
}
|
||
|
||
const QString managerSerialized = printMessage(manager);
|
||
if (managerSerialized.isEmpty()) {
|
||
if (error)
|
||
*error = QStringLiteral("无法序列化 DeviceManager 配置");
|
||
return false;
|
||
}
|
||
m_overviewSaveContents.insert(m_managerPath, managerSerialized);
|
||
m_overviewSaveRevisions.insert(m_managerPath, m_managerRevision);
|
||
m_overviewSaveQueue = m_overviewSaveContents.keys();
|
||
m_overviewSaveQueue.removeAll(m_managerPath);
|
||
std::sort(m_overviewSaveQueue.begin(), m_overviewSaveQueue.end());
|
||
m_overviewSaveQueue.append(m_managerPath);
|
||
return true;
|
||
}
|
||
|
||
void ConfigEditorWidget::finishDeviceEditing()
|
||
{
|
||
if (m_demoMode) {
|
||
QVariantList demoDevices;
|
||
for (const QVariant &value : m_editDevices) {
|
||
const QVariantMap configured = value.toMap();
|
||
QVariantMap live;
|
||
live.insert(QStringLiteral("deviceId"), configured.value(QStringLiteral("deviceId")));
|
||
live.insert(QStringLiteral("type"), configured.value(QStringLiteral("type")));
|
||
live.insert(QStringLiteral("typeName"), QStringLiteral("DEVICE_TYPE_DEMO"));
|
||
demoDevices.append(live);
|
||
}
|
||
m_devices = demoDevices;
|
||
cancelDeviceEditing();
|
||
QMessageBox::information(this, QStringLiteral("演示模式"),
|
||
QStringLiteral("演示配置已应用到当前界面;未写入远端文件。"));
|
||
return;
|
||
}
|
||
QString error;
|
||
if (!prepareOverviewSave(&error)) {
|
||
QMessageBox::warning(this, QStringLiteral("无法保存设备配置"), error);
|
||
return;
|
||
}
|
||
m_overviewSaving = true;
|
||
m_deviceEditButton->setEnabled(false);
|
||
m_deviceEditButton->setText(QStringLiteral("保存中…"));
|
||
saveNextOverviewConfiguration();
|
||
}
|
||
|
||
void ConfigEditorWidget::saveNextOverviewConfiguration()
|
||
{
|
||
if (m_overviewSaveQueue.isEmpty()) {
|
||
m_overviewSaving = false;
|
||
m_overviewCurrentSave.clear();
|
||
cancelDeviceEditing();
|
||
m_client->requestSystemRefresh();
|
||
QMessageBox::information(
|
||
this, QStringLiteral("设备配置已保存"),
|
||
QStringLiteral("配置文件已上传到 cmvr-es。配置重载/重启功能将在后续接入。"));
|
||
return;
|
||
}
|
||
m_overviewCurrentSave = m_overviewSaveQueue.takeFirst();
|
||
m_client->saveConfigurationFile(
|
||
m_overviewCurrentSave,
|
||
m_overviewSaveContents.value(m_overviewCurrentSave),
|
||
m_overviewSaveRevisions.value(m_overviewCurrentSave));
|
||
}
|
||
|
||
void ConfigEditorWidget::toggleConnection()
|
||
{
|
||
if (m_connected) {
|
||
if (!confirmDiscardChanges())
|
||
return;
|
||
m_client->disconnectConfigurationServer();
|
||
return;
|
||
}
|
||
if (m_hostEdit->text().trimmed().isEmpty()) {
|
||
setStatus(QStringLiteral("设备地址不能为空"), true);
|
||
return;
|
||
}
|
||
QSettings settings;
|
||
settings.setValue(QStringLiteral("configuration/host"), m_hostEdit->text().trimmed());
|
||
settings.setValue(QStringLiteral("configuration/port"), m_portSpin->value());
|
||
setBusy(true);
|
||
setStatus(QStringLiteral("正在连接配置服务..."));
|
||
m_client->connectConfigurationServer(m_hostEdit->text(), m_portSpin->value());
|
||
}
|
||
|
||
void ConfigEditorWidget::refreshFiles()
|
||
{
|
||
if (!m_connected || !confirmDiscardChanges())
|
||
return;
|
||
setBusy(true);
|
||
setStatus(QStringLiteral("正在读取远端配置列表..."));
|
||
m_client->requestConfigurationFiles();
|
||
}
|
||
|
||
void ConfigEditorWidget::selectFile(QListWidgetItem *current, QListWidgetItem *previous)
|
||
{
|
||
if (!current)
|
||
return;
|
||
const QString nextPath = current->data(kPathRole).toString();
|
||
if (nextPath == m_currentPath)
|
||
return;
|
||
if (!confirmDiscardChanges()) {
|
||
const QSignalBlocker blocker(m_fileList);
|
||
m_fileList->setCurrentItem(previous);
|
||
return;
|
||
}
|
||
setBusy(true);
|
||
setStatus(QStringLiteral("正在读取 %1...").arg(nextPath));
|
||
m_client->readConfigurationFile(nextPath);
|
||
}
|
||
|
||
void ConfigEditorWidget::reloadCurrentFile()
|
||
{
|
||
if (m_currentPath.isEmpty() || !confirmDiscardChanges())
|
||
return;
|
||
setBusy(true);
|
||
m_client->readConfigurationFile(m_currentPath);
|
||
}
|
||
|
||
void ConfigEditorWidget::validateCurrentFile()
|
||
{
|
||
if (m_currentPath.isEmpty())
|
||
return;
|
||
setBusy(true);
|
||
setStatus(QStringLiteral("正在校验 %1...").arg(m_currentPath));
|
||
m_client->validateConfigurationFile(m_currentPath, m_textEditor->toPlainText());
|
||
}
|
||
|
||
void ConfigEditorWidget::saveCurrentFile()
|
||
{
|
||
if (m_currentPath.isEmpty() || m_revision.isEmpty())
|
||
return;
|
||
setBusy(true);
|
||
setStatus(QStringLiteral("正在保存 %1...").arg(m_currentPath));
|
||
m_client->saveConfigurationFile(m_currentPath, m_textEditor->toPlainText(), m_revision);
|
||
}
|
||
|
||
void ConfigEditorWidget::handleConnectionChanged(bool connected, const QString &detail)
|
||
{
|
||
m_connected = connected;
|
||
setBusy(false);
|
||
m_hostEdit->setEnabled(!connected);
|
||
m_portSpin->setEnabled(!connected);
|
||
m_connectButton->setText(connected ? QStringLiteral("断开") : QStringLiteral("连接"));
|
||
setStatus(detail, !connected && detail != QStringLiteral("配置服务已断开"));
|
||
if (connected && m_deviceEditRequested) {
|
||
beginDeviceConfigurationLoad();
|
||
} else if (connected) {
|
||
refreshFiles();
|
||
}
|
||
else {
|
||
if (m_deviceEditRequested || m_overviewLoading) {
|
||
m_deviceEditRequested = false;
|
||
m_overviewLoading = false;
|
||
m_deviceEditButton->setEnabled(m_edgeConnected);
|
||
m_deviceEditButton->setText(QStringLiteral("修改"));
|
||
}
|
||
m_fileList->clear();
|
||
m_rootLabel->setText(QStringLiteral("远端根目录:--"));
|
||
clearCurrentFile();
|
||
}
|
||
updateActions();
|
||
}
|
||
|
||
void ConfigEditorWidget::handleFilesReceived(const QString &configRoot,
|
||
const QVariantList &files,
|
||
bool success, const QString &detail)
|
||
{
|
||
setBusy(false);
|
||
if (!success) {
|
||
if (m_overviewLoading) {
|
||
m_overviewLoading = false;
|
||
m_deviceEditButton->setEnabled(true);
|
||
m_deviceEditButton->setText(QStringLiteral("修改"));
|
||
}
|
||
setStatus(detail.isEmpty() ? QStringLiteral("读取配置列表失败") : detail, true);
|
||
return;
|
||
}
|
||
if (m_overviewLoading) {
|
||
Q_UNUSED(configRoot)
|
||
m_configurationFiles = files;
|
||
m_overviewReadQueue.clear();
|
||
m_managerPath.clear();
|
||
const QSet<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;
|
||
}
|