EXPERIMENT / 配套代码
integration/ptzpanel.cpp
← 返回文章
PtzPanel::PtzPanel(QWidget *parent)
: QWidget(parent)
{
auto *outer = new QVBoxLayout(this);
outer->setContentsMargins(0, 0, 0, 0);
auto *group = new QGroupBox("云台控制", this);
outer->addWidget(group);
auto *grid = new QGridLayout(group);
const QString arrows[] = {"←", "→", "↑", "↓"};
const int rows[] = {1, 1, 0, 2}, cols[] = {0, 2, 1, 1};
for (unsigned i = 0; i < 4; ++i) {
directions[i] = new QPushButton(arrows[i], group);
directions[i]->setMinimumHeight(36);
grid->addWidget(directions[i], rows[i], cols[i]);
unsigned axis = i < 2 ? 0 : 1;
bool inc = (i == 0 || i == 3);
connect(directions[i], &QPushButton::pressed, this, [this, axis, inc] { begin(axis, inc); });
connect(directions[i], &QPushButton::released, this, [this, axis] { release(axis); });
}
auto *stop = new QPushButton("停止", group);
grid->addWidget(stop, 1, 1);
connect(stop, &QPushButton::clicked, this, [this] {
release(0);
release(1);
emit command("ptz_stop", {{"axis", "both"}});
});
speed = new QComboBox(group);
for (int i = 1; i <= 5; ++i)
speed->addItem(QString("%1 档(%2 步/秒)").arg(i).arg(i * 100), i);
grid->addWidget(speed, 3, 0, 1, 3);
home = new QPushButton("校准并回中", group);
center = new QPushButton("回中", group);
grid->addWidget(home, 4, 0, 1, 2);
grid->addWidget(center, 4, 2);
connect(home, &QPushButton::clicked, this, [this] {
release(0);
release(1);
emit command("ptz_home", {{"axis", "both"}, {"level", speed->currentData().toInt()}});
});
connect(center, &QPushButton::clicked, this, [this] {
release(0);
release(1);
emit command("ptz_center", {{"axis", "both"}, {"level", speed->currentData().toInt()}});
});
for (unsigned i = 0; i < 2; ++i) {
grid->addWidget(new QLabel(i ? "垂直目标" : "水平目标", group), 5 + i, 0);
target[i] = new QSpinBox(group);
target[i]->setRange(0, 0);
grid->addWidget(target[i], 5 + i, 1);
move[i] = new QPushButton("移动", group);
grid->addWidget(move[i], 5 + i, 2);
connect(move[i], &QPushButton::clicked, this, [this, i] {
release(i);
emit command("ptz_move_to", {{"axis", i ? "v" : "h"}, {"position", target[i]->value()}, {"level", speed->currentData().toInt()}});
});
}
status = new QLabel("等待设备云台状态", group);
status->setWordWrap(true);
error = new QLabel(group);
error->setWordWrap(true);
grid->addWidget(status, 7, 0, 1, 3);
grid->addWidget(error, 8, 0, 1, 3);
auto *ranges = new QGroupBox("行程配置", this);
outer->addWidget(ranges);
auto *rangeGrid = new QGridLayout(ranges);
for (unsigned i = 0; i < 2; ++i) {
rangeGrid->addWidget(new QLabel(i ? "垂直最大角度" : "水平最大角度", ranges), i, 0);
maxAngle[i] = new QDoubleSpinBox(ranges);
maxAngle[i]->setObjectName(i ? "ptzVerticalAngle" : "ptzHorizontalAngle");
maxAngle[i]->setDecimals(2);
maxAngle[i]->setRange(0.1, 360.0);
maxAngle[i]->setSuffix("°");
maxAngle[i]->setEnabled(false);
rangeGrid->addWidget(maxAngle[i], i, 1);
}
auto *advanced = new QGroupBox("高级:归零寻找行程(半步)", ranges);
advanced->setCheckable(true);
advanced->setChecked(false);
advanced->setToolTip("寻找机械零点的行程,独立于正常运动范围;不能随最大角度自动缩小。");
auto *advancedGrid = new QGridLayout(advanced);
for (unsigned i = 0; i < 2; ++i) {
advancedGrid->addWidget(new QLabel(i ? "垂直" : "水平", advanced), i, 0);
seekSteps[i] = new QSpinBox(advanced);
seekSteps[i]->setRange(1, 60000);
seekSteps[i]->setObjectName(i ? "ptzVerticalSeek" : "ptzHorizontalSeek");
advancedGrid->addWidget(seekSteps[i], i, 1);
}
rangeGrid->addWidget(advanced, 2, 0, 1, 2);
readConfig = new QPushButton("读取配置", ranges);
saveConfig = new QPushButton("保存并应用", ranges);
rangeGrid->addWidget(readConfig, 3, 0);
rangeGrid->addWidget(saveConfig, 3, 1);
configNote = new QLabel("读取设备配置后修改;保存不会自动移动", ranges);
configNote->setWordWrap(true);
rangeGrid->addWidget(configNote, 4, 0, 1, 2);
connect(readConfig, &QPushButton::clicked, this, &PtzPanel::requestConfig);
connect(saveConfig, &QPushButton::clicked, this, [this] {
QJsonObject params{{"expected", configSnapshot}};
for (unsigned i = 0; i < 2; ++i)
params.insert(i ? "v" : "h", QJsonObject{{"max_angle", maxAngle[i]->value()}, {"seek_steps", seekSteps[i]->value()}});
configPending = true;
configWait.restart();
disable();
configNote->setText("正在保存并应用…");
emit command("ptz_set_config", params);
});
disable();
timer = new QTimer(this);
timer->setInterval(200);
connect(timer, &QTimer::timeout, this, &PtzPanel::tick);
timer->start();
}
void PtzPanel::disable()
{
for (auto *b: directions)
b->setEnabled(false);
for (auto *b: move)
b->setEnabled(false);
home->setEnabled(false);
center->setEnabled(false);
readConfig->setEnabled(false);
saveConfig->setEnabled(false);
}
void PtzPanel::requestConfig()
{
configRequested = true;
configPending = true;
configWait.restart();
disable();
configNote->setText("正在读取配置…");
emit command("ptz_get_config", {});
}
bool PtzPanel::applyConfig(const QJsonObject &c)
{
if (!c.value("h").isObject() || !c.value("v").isObject()) {
configSnapshot = {};
configNote->setText("设备未返回有效行程配置");
return false;
}
configSnapshot = c;
for (unsigned i = 0; i < 2; ++i) {
const auto a = c.value(i ? "v" : "h").toObject();
maxAngle[i]->setValue(a.value("max_angle").toDouble());
maxAngle[i]->setEnabled(true);
seekSteps[i]->setValue(a.value("seek_steps").toInt());
}
return true;
}
void PtzPanel::begin(unsigned axis, bool inc)
{
release(axis);
auto &h = held[axis];
h.token = QUuid::createUuid().toString(QUuid::WithoutBraces);
h.increasing = inc;
h.level = speed->currentData().toUInt();
emit command("ptz_manual", {{"axis", axis ? "v" : "h"}, {"increasing", inc}, {"level", int(h.level)}, {"token", h.token}, {"renew", false}});
}
void PtzPanel::release(unsigned axis)
{
if (held[axis].token.isEmpty())
return;
QString token = held[axis].token;
held[axis].token.clear();
emit command("ptz_release", {{"axis", axis ? "v" : "h"}, {"token", token}});
}
void PtzPanel::tick()
{
if (!isVisible())
return;
if (configPending && configWait.elapsed() > 15000) {
configPending = false;
configSnapshot = {};
configNote->setText("配置请求超时,保存结果未知,请重新读取");
}
if (fresh.isValid() && fresh.elapsed() > 2000) {
release(0);
release(1);
disable();
status->setText("云台状态超时,控制已暂停");
}
for (unsigned i = 0; i < 2; ++i) {
const auto &h = held[i];
if (h.token.isEmpty())
continue;
emit command("ptz_manual", {{"axis", i ? "v" : "h"}, {"increasing", h.increasing}, {"level", int(h.level)}, {"token", h.token}, {"renew", true}});
}
if (++ticks % 3 == 1)
emit command("ptz_status", {});
}
void PtzPanel::receive(const QString &cmd, const QJsonObject &reply)
{
if (cmd == "ptz_get_config_ack" || cmd == "ptz_set_config_ack") {
if (reply.value("phase").toString() == "accepted")
return;
configPending = false;
if (reply.value("result").toString() == "ok") {
if (applyConfig(reply.value("ptz_config").toObject()))
configNote->setText(cmd == "ptz_set_config_ack" ? "已保存并应用" : "已读取设备当前配置");
} else {
configSnapshot = {};
configNote->setText(reply.value("error_msg").toString("配置操作失败,请重新读取"));
}
return;
}
if (reply.value("result").toString() != "ok") {
error->setText(reply.value("error_msg").toString("云台命令失败"));
if (cmd == "ptz_manual_ack") {
release(0);
release(1);
}
return;
}
if (cmd != "ptz_status_ack") {
if (cmd != "ptz_manual_ack" && cmd != "ptz_release_ack")
error->clear();
return; // 操作接受不代表到位;只用状态查询刷新显示
}
fresh.restart();
const auto p = reply.value("ptz").toObject();
if (!p.value("supported").toBool() || !p.value("available").toBool()) {
release(0);
release(1);
disable();
status->setText(p.value("supported").toBool() ? "云台配置或驱动不可用" : "固件未启用云台功能");
return;
}
bool ready[2] = {false, false}, confirmed = true, calibrating = false, running = false;
if (!defaultApplied) {
int index = speed->findData(p.value("default_level").toInt(1));
if (index >= 0)
speed->setCurrentIndex(index);
defaultApplied = true;
}
QStringList lines;
for (unsigned i = 0; i < 2; ++i) {
const auto a = p.value(i ? "v" : "h").toObject();
const QString phase = a.value("phase").toString();
bool busy = phase == "seeking" || phase == "centering";
ready[i] = a.value("calibrated").toBool() && !busy && !configPending;
running |= a.value("running").toBool();
confirmed &= a.value("parameters_confirmed").toBool();
calibrating |= busy;
target[i]->setMaximum(a.value("max_steps").toInt());
move[i]->setEnabled(ready[i]);
QString state = phase == "seeking" ? "归零中" : phase == "centering" ? "回中中"
: phase == "fault" ? "故障"
: ready[i] ? "已校准"
: "未校准";
lines << QString("%1:%2 → %3,%4%5")
.arg(i ? "垂直" : "水平")
.arg(a.value("position").toInteger())
.arg(a.value("target").toInteger())
.arg(state)
.arg(a.value("running").toBool() ? ",运行中" : "");
}
directions[0]->setEnabled(ready[0]);
directions[1]->setEnabled(ready[0]);
directions[2]->setEnabled(ready[1]);
directions[3]->setEnabled(ready[1]);
home->setEnabled(confirmed && !calibrating && !configPending);
center->setEnabled(ready[0] && ready[1]);
readConfig->setEnabled(!configPending);
saveConfig->setEnabled(!configPending && !configSnapshot.isEmpty() && !calibrating && !running);
if (!confirmed)
lines << "归零方向和行程参数尚未确认";
status->setText(lines.join("\n"));
if (!configRequested)
requestConfig();
}
void PtzPanel::shutdown()
{
timer->stop();
release(0);
release(1);
emit command("ptz_stop", {{"axis", "both"}});
}
void PtzPanel::hideEvent(QHideEvent *event)
{
release(0);
release(1);
QWidget::hideEvent(event);
}