From 839ce495de8c6ceb1c0bff0114df524e0b1bc180 Mon Sep 17 00:00:00 2001 From: suyu <1643689728@qq.com> Date: Tue, 25 Aug 2026 09:12:33 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=20PLC=20=E4=B8=B2?= =?UTF-8?q?=E5=8F=A3=E8=87=AA=E5=8A=A8=E6=90=9C=E7=B4=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/integrated_platform.pro | 4 + .../infrastructure/plc_discovery_service.cpp | 304 ++++++++++++++++++ .../infrastructure/plc_discovery_service.h | 68 ++++ app/src/main.cpp | 6 +- app/src/services/plc_discovery_gateway.cpp | 126 ++++++++ app/src/services/plc_discovery_gateway.h | 53 +++ app/src/ui/main_window.cpp | 94 +++++- app/src/ui/main_window.h | 5 + app/src/ui/plc_connection_dialog.cpp | 195 ++++++++++- app/src/ui/plc_connection_dialog.h | 22 ++ app/src/ui/plc_connection_dialog.ui | 120 ++++--- app/tests/plc_connection_dialog_tests.cpp | 203 ++++++++++++ app/tests/plc_connection_dialog_tests.pro | 18 ++ app/tests/plc_runtime_tests.cpp | 70 ++++ app/tests/pri/test_layers.pri | 12 +- app/tests/tests.pro | 2 + 16 files changed, 1235 insertions(+), 67 deletions(-) create mode 100644 app/src/infrastructure/plc_discovery_service.cpp create mode 100644 app/src/infrastructure/plc_discovery_service.h create mode 100644 app/src/services/plc_discovery_gateway.cpp create mode 100644 app/src/services/plc_discovery_gateway.h create mode 100644 app/tests/plc_connection_dialog_tests.cpp create mode 100644 app/tests/plc_connection_dialog_tests.pro diff --git a/app/integrated_platform.pro b/app/integrated_platform.pro index 7b153b9..39053e0 100644 --- a/app/integrated_platform.pro +++ b/app/integrated_platform.pro @@ -48,11 +48,13 @@ SOURCES += \ src/services/software_logic_executor.cpp \ src/services/offline_simulation_service.cpp \ src/services/runtime_mode_service.cpp \ + src/services/plc_discovery_gateway.cpp \ src/services/register_monitor_service.cpp \ src/services/register_comment_service.cpp \ src/infrastructure/plc_register_repository.cpp \ src/infrastructure/plc_communication_error_classifier.cpp \ src/infrastructure/plc_communication_service.cpp \ + src/infrastructure/plc_discovery_service.cpp \ src/infrastructure/json_project_storage.cpp \ src/ui/hmi_editor_widget.cpp \ src/ui/logic_editor_widget.cpp @@ -94,12 +96,14 @@ HEADERS += \ src/services/software_logic_executor.h \ src/services/offline_simulation_service.h \ src/services/runtime_mode_service.h \ + src/services/plc_discovery_gateway.h \ src/services/register_monitor_service.h \ src/services/register_comment_service.h \ src/services/plc_communication_gateway.h \ src/infrastructure/plc_register_repository.h \ src/infrastructure/plc_communication_error_classifier.h \ src/infrastructure/plc_communication_service.h \ + src/infrastructure/plc_discovery_service.h \ src/infrastructure/json_project_storage.h \ src/ui/hmi_editor_widget.h \ src/ui/logic_editor_widget.h diff --git a/app/src/infrastructure/plc_discovery_service.cpp b/app/src/infrastructure/plc_discovery_service.cpp new file mode 100644 index 0000000..3a8aa4c --- /dev/null +++ b/app/src/infrastructure/plc_discovery_service.cpp @@ -0,0 +1,304 @@ +#include "plc_discovery_service.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace { + +// 本地 RTU 探测无需沿用正式轮询的长超时和重试,否则多端口搜索会过慢 +constexpr int kDiscoveryResponseTimeoutMs = 200; +// 正式连接可能刚被配置窗口断开,先留出时间让操作系统完全释放串口句柄 +constexpr int kDiscoveryStartDelayMs = 100; + +std::string toUtf8(const QString &value) +{ + const QByteArray bytes = value.toUtf8(); + return std::string(bytes.constData(), static_cast(bytes.size())); +} + +} // namespace + +PlcDiscoveryService::PlcDiscoveryService(QObject *parent) + : QObject(parent), + master_(std::make_unique()) +{ + connect( + master_.get(), &QModbusClient::stateChanged, + this, &PlcDiscoveryService::handleDeviceStateChanged); +} + +PlcDiscoveryService::~PlcDiscoveryService() +{ + progress_changed_callback_ = {}; + discovery_finished_callback_ = {}; + if (master_->state() != QModbusDevice::UnconnectedState) + { + master_->disconnectDevice(); + } +} + +PlcCommunicationResult PlcDiscoveryService::startDiscovery( + const PlcSerialConfiguration &preferred) +{ + if (discovering_) + { + return {false, "PLC 自动搜索正在进行"}; + } + PlcSerialConfiguration validation_configuration = preferred; + validation_configuration.portName = "DISCOVERY"; + const PlcCommunicationResult validation = + validatePlcSerialConfiguration(validation_configuration); + if (!validation.succeeded) + { + return validation; + } + + std::vector available_ports; + for (const QSerialPortInfo &port : QSerialPortInfo::availablePorts()) + { + available_ports.push_back(toUtf8(port.portName())); + } + candidates_ = buildPlcDiscoveryCandidates(preferred, available_ports); + if (candidates_.empty()) + { + return {false, "没有检测到可用串口"}; + } + if (master_->state() != QModbusDevice::UnconnectedState) + { + return {false, "自动搜索串口尚未释放,请稍后重试"}; + } + + current_candidate_ = 0; + pending_reply_.clear(); + phase_ = Phase::Idle; + discovering_ = true; + cancel_requested_ = false; + found_pending_ = false; + ++attempt_generation_; + QTimer::singleShot( + kDiscoveryStartDelayMs, + this, + &PlcDiscoveryService::tryCurrentCandidate); + return {true, {}}; +} + +void PlcDiscoveryService::cancelDiscovery() +{ + if (!discovering_ || cancel_requested_) + { + return; + } + cancel_requested_ = true; + found_pending_ = false; + ++attempt_generation_; + disconnectCurrent(false); +} + +bool PlcDiscoveryService::isDiscovering() const +{ + return discovering_; +} + +void PlcDiscoveryService::setCallbacks( + std::function progress_changed, + std::function discovery_finished) +{ + progress_changed_callback_ = std::move(progress_changed); + discovery_finished_callback_ = std::move(discovery_finished); +} + +void PlcDiscoveryService::tryCurrentCandidate() +{ + if (!discovering_) + { + return; + } + if (cancel_requested_) + { + disconnectCurrent(false); + return; + } + if (current_candidate_ >= candidates_.size()) + { + finishDiscovery({ + false, false, {}, + "未找到可响应的 PLC,请检查站号、接线和 PLC RTU 设置"}); + return; + } + + const PlcSerialConfiguration &candidate = candidates_.at(current_candidate_); + if (progress_changed_callback_) + { + progress_changed_callback_({ + candidate, current_candidate_ + 1U, candidates_.size()}); + } + ++attempt_generation_; + phase_ = Phase::Connecting; + master_->setConnectionParameter( + QModbusDevice::SerialPortNameParameter, + QString::fromUtf8(candidate.portName.c_str())); + master_->setConnectionParameter( + QModbusDevice::SerialBaudRateParameter, candidate.baudRate); + master_->setConnectionParameter( + QModbusDevice::SerialDataBitsParameter, + static_cast(candidate.dataBits)); + master_->setConnectionParameter( + QModbusDevice::SerialParityParameter, + static_cast(candidate.parity)); + master_->setConnectionParameter( + QModbusDevice::SerialStopBitsParameter, + static_cast(candidate.stopBits)); + master_->setTimeout(kDiscoveryResponseTimeoutMs); + master_->setNumberOfRetries(0); + if (!master_->connectDevice()) + { + QTimer::singleShot( + 0, this, + [this] + { + if (discovering_ && phase_ == Phase::Connecting) + { + disconnectCurrent(false); + } + }); + } +} + +void PlcDiscoveryService::sendProbe() +{ + if (!discovering_ || cancel_requested_ + || current_candidate_ >= candidates_.size()) + { + disconnectCurrent(false); + return; + } + phase_ = Phase::Probing; + const PlcSerialConfiguration &candidate = candidates_.at(current_candidate_); + const QModbusDataUnit request( + QModbusDataUnit::HoldingRegisters, 0, 1); + QModbusReply *reply = master_->sendReadRequest( + request, candidate.serverAddress); + if (reply == nullptr) + { + disconnectCurrent(false); + return; + } + pending_reply_ = reply; + const std::uint64_t generation = attempt_generation_; + connect( + reply, &QModbusReply::finished, + this, + [this, reply, generation] + { + handleProbeFinished(reply, generation); + }); +} + +void PlcDiscoveryService::handleProbeFinished( + QModbusReply *reply, + std::uint64_t attempt_generation) +{ + const bool is_current_attempt = discovering_ + && !cancel_requested_ + && phase_ == Phase::Probing + && attempt_generation == attempt_generation_; + if (pending_reply_ == reply) + { + pending_reply_.clear(); + } + const bool valid_response = reply->error() == QModbusDevice::NoError + || reply->rawResult().isException(); + reply->deleteLater(); + if (is_current_attempt) + { + disconnectCurrent(valid_response); + } +} + +void PlcDiscoveryService::disconnectCurrent(bool found) +{ + if (!discovering_) + { + return; + } + found_pending_ = found && !cancel_requested_; + phase_ = Phase::Disconnecting; + if (master_->state() == QModbusDevice::UnconnectedState) + { + QTimer::singleShot(0, this, &PlcDiscoveryService::continueAfterDisconnect); + return; + } + master_->disconnectDevice(); +} + +void PlcDiscoveryService::continueAfterDisconnect() +{ + if (!discovering_ || phase_ != Phase::Disconnecting) + { + return; + } + if (cancel_requested_) + { + finishDiscovery({false, true, {}, {}}); + return; + } + if (found_pending_) + { + finishDiscovery({ + true, false, candidates_.at(current_candidate_), + "已找到 PLC,正在连接"}); + return; + } + ++current_candidate_; + phase_ = Phase::Idle; + QTimer::singleShot(0, this, &PlcDiscoveryService::tryCurrentCandidate); +} + +void PlcDiscoveryService::finishDiscovery(const PlcDiscoveryOutcome &outcome) +{ + discovering_ = false; + cancel_requested_ = false; + found_pending_ = false; + pending_reply_.clear(); + phase_ = Phase::Idle; + ++attempt_generation_; + if (discovery_finished_callback_) + { + discovery_finished_callback_(outcome); + } + candidates_.clear(); + current_candidate_ = 0; +} + +void PlcDiscoveryService::handleDeviceStateChanged(QModbusDevice::State state) +{ + if (!discovering_) + { + return; + } + if (state == QModbusDevice::ConnectedState && phase_ == Phase::Connecting) + { + sendProbe(); + return; + } + if (state == QModbusDevice::UnconnectedState) + { + if (phase_ == Phase::Disconnecting) + { + QTimer::singleShot(0, this, &PlcDiscoveryService::continueAfterDisconnect); + } + else if (phase_ == Phase::Connecting || phase_ == Phase::Probing) + { + disconnectCurrent(false); + } + } +} diff --git a/app/src/infrastructure/plc_discovery_service.h b/app/src/infrastructure/plc_discovery_service.h new file mode 100644 index 0000000..c4ffe55 --- /dev/null +++ b/app/src/infrastructure/plc_discovery_service.h @@ -0,0 +1,68 @@ +#pragma once + +#include "services/plc_discovery_gateway.h" + +#include +#include +#include + +#include +#include +#include +#include +#include + +class QModbusReply; +class QModbusRtuSerialMaster; + +// 使用独立 Qt Modbus RTU 主站逐项探测串口参数,不污染正式 PLC 连接状态 +class PlcDiscoveryService final : public QObject, public PlcDiscoveryGateway +{ +public: + explicit PlcDiscoveryService(QObject *parent = nullptr); + ~PlcDiscoveryService() override; + + PlcCommunicationResult startDiscovery( + const PlcSerialConfiguration &preferred) override; + void cancelDiscovery() override; + bool isDiscovering() const override; + void setCallbacks( + std::function progress_changed, + std::function discovery_finished) override; + +private: + enum class Phase + { + Idle, + Connecting, + Probing, + Disconnecting + }; + + // 尝试当前候选参数并等待串口异步打开 + void tryCurrentCandidate(); + // 串口打开后发送只读 D0 探测请求 + void sendProbe(); + // 处理当前 D0 探测结果 + void handleProbeFinished(QModbusReply *reply, std::uint64_t attempt_generation); + // 释放当前串口,释放完成后进入下一个候选或报告结果 + void disconnectCurrent(bool found); + // 当前串口完全释放后完成状态迁移 + void continueAfterDisconnect(); + // 报告最终结果并清理搜索状态 + void finishDiscovery(const PlcDiscoveryOutcome &outcome); + // 处理 Qt Modbus 主站连接状态变化 + void handleDeviceStateChanged(QModbusDevice::State state); + + std::unique_ptr master_; + std::vector candidates_; + std::size_t current_candidate_ = 0; + QPointer pending_reply_; + Phase phase_ = Phase::Idle; + bool discovering_ = false; + bool cancel_requested_ = false; + bool found_pending_ = false; + std::uint64_t attempt_generation_ = 0; + std::function progress_changed_callback_; + std::function discovery_finished_callback_; +}; diff --git a/app/src/main.cpp b/app/src/main.cpp index 6376930..80d8f5d 100644 --- a/app/src/main.cpp +++ b/app/src/main.cpp @@ -10,6 +10,7 @@ #include "infrastructure/json_project_storage.h" #include "infrastructure/plc_communication_service.h" +#include "infrastructure/plc_discovery_service.h" #include "infrastructure/plc_register_repository.h" #include "domain/active_register_repository.h" #include "domain/virtual_register_repository.h" @@ -57,6 +58,8 @@ int main(int argc, char *argv[]) ActiveRegisterRepository active_register_repository(virtual_register_repository); // 负责通过串口异步连接、读取和写入真实 PLC PlcCommunicationService plc_communication_service(plc_register_repository); + // 使用独立串口会话自动搜索 PLC,不影响正式通信状态机 + PlcDiscoveryService plc_discovery_service; // 处理运行画面读取和写入寄存器,数据来自当前选中的寄存器入口 HmiRuntimeService hmi_runtime_service(active_register_repository); @@ -91,7 +94,8 @@ int main(int argc, char *argv[]) alarm_service, hmi_navigation_service, register_comment_service, - register_monitor_service); + register_monitor_service, + plc_discovery_service); // 把主窗口显示到屏幕上 main_window.show(); diff --git a/app/src/services/plc_discovery_gateway.cpp b/app/src/services/plc_discovery_gateway.cpp new file mode 100644 index 0000000..f6fb941 --- /dev/null +++ b/app/src/services/plc_discovery_gateway.cpp @@ -0,0 +1,126 @@ +#include "plc_discovery_gateway.h" + +#include +#include +#include +#include + +namespace { + +struct SerialFrame +{ + int dataBits = 8; + int parity = 2; + int stopBits = 1; +}; + +bool isSupportedBaudRate(int baud_rate) +{ + return baud_rate == 9600 + || baud_rate == 19200 + || baud_rate == 38400 + || baud_rate == 57600 + || baud_rate == 115200; +} + +bool isSupportedFrame(const SerialFrame &frame) +{ + return (frame.dataBits == 7 || frame.dataBits == 8) + && (frame.parity == 0 || frame.parity == 2 || frame.parity == 3) + && (frame.stopBits == 1 || frame.stopBits == 2); +} + +bool sameFrame(const SerialFrame &left, const SerialFrame &right) +{ + return std::tie(left.dataBits, left.parity, left.stopBits) + == std::tie(right.dataBits, right.parity, right.stopBits); +} + +template +void appendUnique(std::vector *values, Value value, Equals equals) +{ + if (std::none_of( + values->cbegin(), values->cend(), + [&value, &equals](const Value &existing) { return equals(existing, value); })) + { + values->push_back(std::move(value)); + } +} + +} // namespace + +std::vector buildPlcDiscoveryCandidates( + const PlcSerialConfiguration &preferred, + const std::vector &available_ports) +{ + std::vector ports; + ports.reserve(available_ports.size()); + for (const std::string &port : available_ports) + { + if (!port.empty()) + { + appendUnique( + &ports, port, + [](const std::string &left, const std::string &right) { + return left == right; + }); + } + } + const auto preferred_port = std::find( + ports.begin(), ports.end(), preferred.portName); + if (preferred_port != ports.end()) + { + std::rotate(ports.begin(), preferred_port, preferred_port + 1); + } + + std::vector baud_rates; + if (isSupportedBaudRate(preferred.baudRate)) + { + baud_rates.push_back(preferred.baudRate); + } + constexpr std::array kSupportedBaudRates{ + 9600, 19200, 38400, 57600, 115200}; + for (int baud_rate : kSupportedBaudRates) + { + appendUnique( + &baud_rates, baud_rate, + [](int left, int right) { return left == right; }); + } + + std::vector frames; + const SerialFrame preferred_frame{ + preferred.dataBits, preferred.parity, preferred.stopBits}; + if (isSupportedFrame(preferred_frame)) + { + frames.push_back(preferred_frame); + } + constexpr std::array kSupportedFrames{{ + {8, 2, 1}, {8, 0, 1}, {8, 3, 1}, + {8, 0, 2}, {8, 2, 2}, {8, 3, 2}, + {7, 2, 1}, {7, 3, 1}, {7, 0, 1}, + {7, 0, 2}, {7, 2, 2}, {7, 3, 2}}}; + for (const SerialFrame &frame : kSupportedFrames) + { + appendUnique(&frames, frame, sameFrame); + } + + std::vector candidates; + candidates.reserve(ports.size() * baud_rates.size() * frames.size()); + for (const SerialFrame &frame : frames) + { + for (int baud_rate : baud_rates) + { + for (const std::string &port : ports) + { + PlcSerialConfiguration candidate = preferred; + candidate.portName = port; + candidate.baudRate = baud_rate; + candidate.dataBits = frame.dataBits; + candidate.parity = frame.parity; + candidate.stopBits = frame.stopBits; + candidates.push_back(std::move(candidate)); + } + } + } + return candidates; +} diff --git a/app/src/services/plc_discovery_gateway.h b/app/src/services/plc_discovery_gateway.h new file mode 100644 index 0000000..9a31468 --- /dev/null +++ b/app/src/services/plc_discovery_gateway.h @@ -0,0 +1,53 @@ +#pragma once + +#include "plc_communication_gateway.h" + +#include +#include +#include +#include + +// 自动搜索正在尝试的串口参数和总体进度 +struct PlcDiscoveryProgress +{ + PlcSerialConfiguration configuration; + std::size_t currentAttempt = 0; + std::size_t totalAttempts = 0; +}; + +// 自动搜索结束结果;found 和 cancelled 不会同时为 true +struct PlcDiscoveryOutcome +{ + bool found = false; + bool cancelled = false; + PlcSerialConfiguration configuration; + std::string message; +}; + +/** + * 按优先级生成搜索组合 + * + * 站号和通信时序沿用界面当前值,只遍历可用端口及项目支持的串口参数 + */ +std::vector buildPlcDiscoveryCandidates( + const PlcSerialConfiguration &preferred, + const std::vector &available_ports); + +// UI 使用的异步 PLC 自动搜索契约,具体串口实现位于 infrastructure +class PlcDiscoveryGateway +{ +public: + virtual ~PlcDiscoveryGateway() = default; + + // 开始搜索,实际进度和结果通过回调通知 + virtual PlcCommunicationResult startDiscovery( + const PlcSerialConfiguration &preferred) = 0; + // 取消当前搜索;串口释放后通过结束回调通知 + virtual void cancelDiscovery() = 0; + // 返回当前是否正在搜索或等待释放搜索串口 + virtual bool isDiscovering() const = 0; + // 替换进度和结束回调,传入空函数可取消通知 + virtual void setCallbacks( + std::function progress_changed, + std::function discovery_finished) = 0; +}; diff --git a/app/src/ui/main_window.cpp b/app/src/ui/main_window.cpp index b4807c1..a34b4e3 100644 --- a/app/src/ui/main_window.cpp +++ b/app/src/ui/main_window.cpp @@ -29,6 +29,7 @@ #include #include #include +#include #include #include #include @@ -41,6 +42,7 @@ #include #include #include +#include #include #include #include @@ -68,6 +70,55 @@ bool isTextEditingObject(QObject *object) return false; } +QString suggestedProjectFileName(QString project_name) +{ + project_name = project_name.trimmed(); + const QString invalid_characters = QStringLiteral("<>:\"/\\|?*"); + for (QChar &character : project_name) + { + if (character.unicode() < 0x20U || invalid_characters.contains(character)) + { + character = QLatin1Char('_'); + } + } + + constexpr int kMaximumSuggestedBaseNameLength = 240; + if (project_name.size() > kMaximumSuggestedBaseNameLength) + { + project_name.truncate(kMaximumSuggestedBaseNameLength); + if (!project_name.isEmpty() + && project_name.at(project_name.size() - 1).isHighSurrogate()) + { + project_name.chop(1); + } + } + while (project_name.endsWith(QLatin1Char(' ')) + || project_name.endsWith(QLatin1Char('.'))) + { + project_name.chop(1); + } + if (project_name.isEmpty()) + { + project_name = QStringLiteral("未命名工程"); + } + + const QString device_name = project_name.section(QLatin1Char('.'), 0, 0).toUpper(); + const bool is_reserved_device_name = device_name == QStringLiteral("CON") + || device_name == QStringLiteral("PRN") + || device_name == QStringLiteral("AUX") + || device_name == QStringLiteral("NUL") + || (device_name.size() == 4 + && (device_name.startsWith(QStringLiteral("COM")) + || device_name.startsWith(QStringLiteral("LPT"))) + && device_name.back() >= QLatin1Char('1') + && device_name.back() <= QLatin1Char('9')); + if (is_reserved_device_name) + { + project_name.prepend(QLatin1Char('_')); + } + return project_name + QStringLiteral(".json"); +} + bool isEditorShortcut(const QKeyEvent &event) { return event.matches(QKeySequence::Undo) @@ -210,6 +261,7 @@ MainWindow::MainWindow( AlarmService &alarm_service, RegisterCommentService ®ister_comment_service, RegisterMonitorService ®ister_monitor_service, + PlcDiscoveryGateway &plc_discovery_gateway, QWidget *parent) : QMainWindow(parent), ui_(std::make_unique()), @@ -222,6 +274,7 @@ MainWindow::MainWindow( alarm_service_(alarm_service), register_comment_service_(register_comment_service), register_monitor_service_(register_monitor_service), + plc_discovery_gateway_(plc_discovery_gateway), owned_hmi_navigation_service_( std::make_unique(project_service)), hmi_navigation_service_(owned_hmi_navigation_service_.get()) @@ -240,6 +293,7 @@ MainWindow::MainWindow( HmiNavigationService &hmi_navigation_service, RegisterCommentService ®ister_comment_service, RegisterMonitorService ®ister_monitor_service, + PlcDiscoveryGateway &plc_discovery_gateway, QWidget *parent) : QMainWindow(parent), ui_(std::make_unique()), @@ -252,6 +306,7 @@ MainWindow::MainWindow( alarm_service_(alarm_service), register_comment_service_(register_comment_service), register_monitor_service_(register_monitor_service), + plc_discovery_gateway_(plc_discovery_gateway), hmi_navigation_service_(&hmi_navigation_service) { initializeUi(); @@ -1204,7 +1259,11 @@ void MainWindow::updateProjectTreeActions() void MainWindow::connectPlc() { - PlcConnectionDialog dialog(plc_configuration_, this); + PlcConnectionDialog dialog( + plc_configuration_, + plc_discovery_gateway_, + [this] { runtime_mode_service_.disconnectPlc(); }, + this); if (dialog.exec() != QDialog::Accepted) { return; @@ -1466,12 +1525,32 @@ void MainWindow::createNewProject() { return; } - const QString name = QInputDialog::getText( - this, tr("新建工程"), tr("工程名称")); - if (name.isEmpty()) + + QInputDialog name_dialog(this); + name_dialog.setWindowTitle(tr("新建工程")); + name_dialog.setLabelText(tr("工程名称")); + name_dialog.setInputMode(QInputDialog::TextInput); + + QDialogButtonBox *button_box = name_dialog.findChild(); + QPushButton *ok_button = button_box != nullptr + ? button_box->button(QDialogButtonBox::Ok) + : nullptr; + if (ok_button != nullptr) + { + ok_button->setEnabled(false); + connect(&name_dialog, + &QInputDialog::textValueChanged, + &name_dialog, + [ok_button](const QString &text) { + ok_button->setEnabled(!text.trimmed().isEmpty()); + }); + } + + if (name_dialog.exec() != QDialog::Accepted) { return; } + const QString name = name_dialog.textValue().trimmed(); const ProjectOperationResult result = project_service_.createNewProject(toUtf8(name)); if (!result.succeeded) { @@ -1512,8 +1591,13 @@ void MainWindow::saveProject() void MainWindow::saveProjectAs() { + const QString suggested_file_name = suggestedProjectFileName( + fromUtf8(project_service_.project().metadata.name)); const QString path = QFileDialog::getSaveFileName( - this, tr("工程另存为"), {}, tr("工程文件 (*.json)")); + this, + tr("工程另存为"), + suggested_file_name, + tr("工程文件 (*.json)")); if (path.isEmpty()) { return; diff --git a/app/src/ui/main_window.h b/app/src/ui/main_window.h index 66d6614..418a8a9 100644 --- a/app/src/ui/main_window.h +++ b/app/src/ui/main_window.h @@ -43,6 +43,7 @@ class HmiEditorWidget; class LogicEditorService; class LogicEditorWidget; class RegisterMonitorService; +class PlcDiscoveryGateway; class RuntimeMonitorWidget; class ProjectWorkspaceController; class PropertyPanelController; @@ -77,6 +78,7 @@ public: AlarmService &alarm_service, RegisterCommentService ®ister_comment_service, RegisterMonitorService ®ister_monitor_service, + PlcDiscoveryGateway &plc_discovery_gateway, QWidget *parent = nullptr); /** * @brief 创建主窗口,并使用调用方提供的 HMI 页面导航服务 @@ -94,6 +96,7 @@ public: HmiNavigationService &hmi_navigation_service, RegisterCommentService ®ister_comment_service, RegisterMonitorService ®ister_monitor_service, + PlcDiscoveryGateway &plc_discovery_gateway, QWidget *parent = nullptr); /** 释放窗口拥有的控制器和可选导航服务 */ @@ -251,6 +254,8 @@ private: RegisterCommentService ®ister_comment_service_; /** 非拥有的寄存器监控服务依赖 */ RegisterMonitorService ®ister_monitor_service_; + /** 非拥有的 PLC 自动搜索服务依赖 */ + PlcDiscoveryGateway &plc_discovery_gateway_; /** 当调用方未提供导航服务时由主窗口负责拥有的实例 */ std::unique_ptr owned_hmi_navigation_service_; /** 当前使用的 HMI 页面导航服务 */ diff --git a/app/src/ui/plc_connection_dialog.cpp b/app/src/ui/plc_connection_dialog.cpp index e974954..db57a5e 100644 --- a/app/src/ui/plc_connection_dialog.cpp +++ b/app/src/ui/plc_connection_dialog.cpp @@ -3,9 +3,17 @@ #include "ui_plc_connection_dialog.h" #include +#include +#include +#include +#include #include +#include +#include +#include #include +#include namespace { @@ -32,9 +40,13 @@ void selectData(QComboBox *combo_box, int value) PlcConnectionDialog::PlcConnectionDialog( const PlcSerialConfiguration &configuration, + PlcDiscoveryGateway &discovery_gateway, + std::function prepare_discovery, QWidget *parent) : QDialog(parent), - ui_(std::make_unique()) + ui_(std::make_unique()), + discovery_gateway_(discovery_gateway), + prepare_discovery_(std::move(prepare_discovery)) { ui_->setupUi(this); setItemData(ui_->baudRateComboBox, {9600, 19200, 38400, 57600, 115200}); @@ -42,22 +54,8 @@ PlcConnectionDialog::PlcConnectionDialog( setItemData(ui_->parityComboBox, {0, 3, 2}); setItemData(ui_->stopBitsComboBox, {1, 2}); - for (const QSerialPortInfo &port : QSerialPortInfo::availablePorts()) - { - ui_->serialPortComboBox->addItem( - port.portName(), - port.description().isEmpty() ? port.portName() : port.description()); - } const QString port_name = QString::fromUtf8(configuration.portName.c_str()); - if (!port_name.isEmpty()) - { - const int port_index = ui_->serialPortComboBox->findText(port_name); - if (port_index < 0) - { - ui_->serialPortComboBox->addItem(port_name); - } - ui_->serialPortComboBox->setCurrentText(port_name); - } + refreshSerialPorts(port_name); ui_->serverAddressSpinBox->setValue(configuration.serverAddress); selectData(ui_->baudRateComboBox, configuration.baudRate); @@ -67,9 +65,36 @@ PlcConnectionDialog::PlcConnectionDialog( ui_->responseTimeoutSpinBox->setValue(configuration.responseTimeoutMs); ui_->retriesSpinBox->setValue(configuration.retries); ui_->pollIntervalSpinBox->setValue(configuration.pollIntervalMs); + + ui_->autoSearchButton->setIcon(style()->standardIcon(QStyle::SP_BrowserReload)); + ui_->refreshPortsButton->setIcon(style()->standardIcon(QStyle::SP_BrowserReload)); + ui_->discoveryProgressBar->setVisible(false); + connect( + ui_->refreshPortsButton, &QToolButton::clicked, + this, + [this] + { + refreshSerialPorts(ui_->serialPortComboBox->currentText().trimmed()); + }); + connect( + ui_->autoSearchButton, &QPushButton::clicked, + this, &PlcConnectionDialog::toggleAutomaticDiscovery); + discovery_gateway_.setCallbacks( + [this](const PlcDiscoveryProgress &progress) + { + showDiscoveryProgress(progress); + }, + [this](const PlcDiscoveryOutcome &outcome) + { + handleDiscoveryFinished(outcome); + }); } -PlcConnectionDialog::~PlcConnectionDialog() = default; +PlcConnectionDialog::~PlcConnectionDialog() +{ + discovery_gateway_.setCallbacks({}, {}); + discovery_gateway_.cancelDiscovery(); +} PlcSerialConfiguration PlcConnectionDialog::configuration() const { @@ -86,3 +111,139 @@ PlcSerialConfiguration PlcConnectionDialog::configuration() const result.pollIntervalMs = ui_->pollIntervalSpinBox->value(); return result; } + +void PlcConnectionDialog::refreshSerialPorts(const QString &preferred_port) +{ + ui_->serialPortComboBox->clear(); + for (const QSerialPortInfo &port : QSerialPortInfo::availablePorts()) + { + ui_->serialPortComboBox->addItem( + port.portName(), + port.description().isEmpty() ? port.portName() : port.description()); + } + const int preferred_index = ui_->serialPortComboBox->findText(preferred_port); + if (preferred_index >= 0) + { + ui_->serialPortComboBox->setCurrentIndex(preferred_index); + } + else if (ui_->serialPortComboBox->count() > 0) + { + ui_->serialPortComboBox->setCurrentIndex(0); + } + else + { + ui_->serialPortComboBox->clearEditText(); + } +} + +void PlcConnectionDialog::toggleAutomaticDiscovery() +{ + if (discovery_gateway_.isDiscovering()) + { + ui_->autoSearchButton->setEnabled(false); + ui_->autoSearchButton->setText(tr("正在停止")); + discovery_gateway_.cancelDiscovery(); + return; + } + const PlcCommunicationResult result = discovery_gateway_.startDiscovery( + configuration()); + if (!result.succeeded) + { + const QString message = QString::fromUtf8(result.message.c_str()); + ui_->discoveryStatusLabel->setText(message); + QMessageBox::information(this, tr("自动搜索 PLC"), message); + return; + } + if (prepare_discovery_) + { + prepare_discovery_(); + } + setDiscoveryUiActive(true); +} + +void PlcConnectionDialog::showDiscoveryProgress( + const PlcDiscoveryProgress &progress) +{ + const PlcSerialConfiguration &candidate = progress.configuration; + const QChar parity = candidate.parity == 2 + ? QLatin1Char('E') + : candidate.parity == 3 ? QLatin1Char('O') : QLatin1Char('N'); + ui_->discoveryProgressBar->setMaximum( + static_cast(progress.totalAttempts)); + ui_->discoveryProgressBar->setValue( + static_cast(progress.currentAttempt)); + ui_->discoveryStatusLabel->setText( + tr("正在搜索 %1:站号 %2,%3,%4%5%6(%7/%8)") + .arg(QString::fromUtf8(candidate.portName.c_str())) + .arg(candidate.serverAddress) + .arg(candidate.baudRate) + .arg(candidate.dataBits) + .arg(parity) + .arg(candidate.stopBits) + .arg(progress.currentAttempt) + .arg(progress.totalAttempts)); +} + +void PlcConnectionDialog::handleDiscoveryFinished( + const PlcDiscoveryOutcome &outcome) +{ + setDiscoveryUiActive(false); + if (outcome.cancelled) + { + ui_->discoveryStatusLabel->setText(tr("已停止自动搜索")); + return; + } + if (!outcome.found) + { + const QString message = outcome.message.empty() + ? tr("未找到可响应的 PLC") + : QString::fromUtf8(outcome.message.c_str()); + ui_->discoveryStatusLabel->setText(message); + QMessageBox::information(this, tr("自动搜索 PLC"), message); + return; + } + applyConfiguration(outcome.configuration); + ui_->discoveryStatusLabel->setText(tr("已找到 PLC,正在连接")); + QTimer::singleShot(0, this, &QDialog::accept); +} + +void PlcConnectionDialog::setDiscoveryUiActive(bool active) +{ + ui_->discoveryProgressBar->setVisible(active); + ui_->serialPortComboBox->setEnabled(!active); + ui_->refreshPortsButton->setEnabled(!active); + ui_->serverAddressSpinBox->setEnabled(!active); + ui_->baudRateComboBox->setEnabled(!active); + ui_->dataBitsComboBox->setEnabled(!active); + ui_->parityComboBox->setEnabled(!active); + ui_->stopBitsComboBox->setEnabled(!active); + ui_->timingGroupBox->setEnabled(!active); + QPushButton *ok_button = ui_->buttonBox->button(QDialogButtonBox::Ok); + if (ok_button != nullptr) + { + ok_button->setEnabled(!active); + } + ui_->autoSearchButton->setEnabled(true); + ui_->autoSearchButton->setText(active ? tr("停止搜索") : tr("自动搜索")); + ui_->autoSearchButton->setIcon(style()->standardIcon( + active ? QStyle::SP_BrowserStop : QStyle::SP_BrowserReload)); +} + +void PlcConnectionDialog::applyConfiguration( + const PlcSerialConfiguration &configuration) +{ + const QString port_name = QString::fromUtf8(configuration.portName.c_str()); + if (ui_->serialPortComboBox->findText(port_name) < 0) + { + ui_->serialPortComboBox->addItem(port_name); + } + ui_->serialPortComboBox->setCurrentText(port_name); + ui_->serverAddressSpinBox->setValue(configuration.serverAddress); + selectData(ui_->baudRateComboBox, configuration.baudRate); + selectData(ui_->dataBitsComboBox, configuration.dataBits); + selectData(ui_->parityComboBox, configuration.parity); + selectData(ui_->stopBitsComboBox, configuration.stopBits); + ui_->responseTimeoutSpinBox->setValue(configuration.responseTimeoutMs); + ui_->retriesSpinBox->setValue(configuration.retries); + ui_->pollIntervalSpinBox->setValue(configuration.pollIntervalMs); +} diff --git a/app/src/ui/plc_connection_dialog.h b/app/src/ui/plc_connection_dialog.h index 4662780..bc4cd2b 100644 --- a/app/src/ui/plc_connection_dialog.h +++ b/app/src/ui/plc_connection_dialog.h @@ -9,9 +9,12 @@ #pragma once #include "services/plc_communication_gateway.h" +#include "services/plc_discovery_gateway.h" #include +#include +#include #include namespace Ui { @@ -27,6 +30,8 @@ public: /** 使用现有配置初始化 PLC 参数表单 */ explicit PlcConnectionDialog( const PlcSerialConfiguration &configuration, + PlcDiscoveryGateway &discovery_gateway, + std::function prepare_discovery, QWidget *parent = nullptr); /** 释放 Designer 界面对象 */ ~PlcConnectionDialog() override; @@ -35,6 +40,23 @@ public: PlcSerialConfiguration configuration() const; private: + /** 重新枚举当前实际存在的串口,并尽量保留仍然可用的选择 */ + void refreshSerialPorts(const QString &preferred_port); + /** 开始自动搜索或请求停止当前搜索 */ + void toggleAutomaticDiscovery(); + /** 根据异步探测进度刷新界面 */ + void showDiscoveryProgress(const PlcDiscoveryProgress &progress); + /** 处理找到、未找到和用户取消三种结束结果 */ + void handleDiscoveryFinished(const PlcDiscoveryOutcome &outcome); + /** 切换搜索期间的表单编辑权限和按钮状态 */ + void setDiscoveryUiActive(bool active); + /** 把一组串口配置回填到界面 */ + void applyConfiguration(const PlcSerialConfiguration &configuration); + /** Qt Designer 生成的界面对象 */ std::unique_ptr ui_; + /** 异步 PLC 自动搜索服务 */ + PlcDiscoveryGateway &discovery_gateway_; + /** 搜索前释放正式 PLC 连接占用的串口 */ + std::function prepare_discovery_; }; diff --git a/app/src/ui/plc_connection_dialog.ui b/app/src/ui/plc_connection_dialog.ui index 4fddecd..5da0ee7 100644 --- a/app/src/ui/plc_connection_dialog.ui +++ b/app/src/ui/plc_connection_dialog.ui @@ -6,54 +6,94 @@ PLC 通信配置 - 4200 + 4400 Modbus RTU 串口 - - QFormLayout::AllNonFixedFieldsGrow - 端口 - - - true - 选择或输入 COM 端口 - - - PLC 站号 - 1247 - 波特率 - - - 96009600 - 1920019200 - 3840038400 - 5760057600 - 115200115200 - + + + + + + 自动搜索 + + + + + 0 + 100 + 0 + false + + + - 数据位 - - - 77 - 88 + + + 自动搜索使用当前 PLC 站号 + true - 校验方式 - - - 无校验0 - 奇校验3 - 偶校验2 - - - 停止位 - - - 11 - 22 - + + + QFormLayout::AllNonFixedFieldsGrow + 端口 + + + + + true + 选择或输入 COM 端口 + + + + + 2828 + 2828 + 刷新串口 + 刷新串口 + true + + + + + PLC 站号 + 1247 + 波特率 + + + 96009600 + 1920019200 + 3840038400 + 5760057600 + 115200115200 + + + 数据位 + + + 77 + 88 + + + 校验方式 + + + 无校验0 + 奇校验3 + 偶校验2 + + + 停止位 + + + 11 + 22 + + + diff --git a/app/tests/plc_connection_dialog_tests.cpp b/app/tests/plc_connection_dialog_tests.cpp new file mode 100644 index 0000000..f3b52a0 --- /dev/null +++ b/app/tests/plc_connection_dialog_tests.cpp @@ -0,0 +1,203 @@ +#include "services/plc_discovery_gateway.h" +#include "support/test_support.h" +#include "ui/plc_connection_dialog.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace { + +using TestSupport::require; + +class FakePlcDiscoveryGateway final : public PlcDiscoveryGateway +{ +public: + PlcCommunicationResult startDiscovery( + const PlcSerialConfiguration &preferred) override + { + ++start_count; + last_preferred = preferred; + discovering = true; + return {true, {}}; + } + + void cancelDiscovery() override + { + if (!discovering) + { + return; + } + ++cancel_count; + discovering = false; + if (finished_callback) + { + finished_callback({false, true, {}, {}}); + } + } + + bool isDiscovering() const override + { + return discovering; + } + + void setCallbacks( + std::function progress_changed, + std::function discovery_finished) override + { + progress_callback = std::move(progress_changed); + finished_callback = std::move(discovery_finished); + } + + void reportProgress( + const PlcSerialConfiguration &candidate, + std::size_t current, + std::size_t total) + { + if (progress_callback) + { + progress_callback({candidate, current, total}); + } + } + + void reportFound(const PlcSerialConfiguration &configuration) + { + discovering = false; + if (finished_callback) + { + finished_callback({true, false, configuration, "found"}); + } + } + + bool discovering = false; + int start_count = 0; + int cancel_count = 0; + PlcSerialConfiguration last_preferred; + std::function progress_callback; + std::function finished_callback; +}; + +void testDiscoveryFindsAndAcceptsConfiguration() +{ + FakePlcDiscoveryGateway gateway; + PlcSerialConfiguration initial; + initial.portName = "COM1"; + int prepare_count = 0; + PlcConnectionDialog dialog( + initial, gateway, [&prepare_count] { ++prepare_count; }); + + QPushButton *search_button = dialog.findChild("autoSearchButton"); + QProgressBar *progress_bar = dialog.findChild( + "discoveryProgressBar"); + QLabel *status_label = dialog.findChild("discoveryStatusLabel"); + require(search_button != nullptr && progress_bar != nullptr + && status_label != nullptr, + "PLC dialog must expose automatic discovery controls"); + + search_button->click(); + require(prepare_count == 1 && gateway.start_count == 1 && gateway.discovering, + "automatic discovery must release the formal connection and start once"); + require(search_button->text() == QStringLiteral("停止搜索") + && !progress_bar->isHidden(), + "the dialog must show searchable progress and a stop command"); + + PlcSerialConfiguration found = initial; + found.portName = "COM3"; + found.serverAddress = 1; + found.baudRate = 19200; + found.dataBits = 8; + found.parity = 0; + found.stopBits = 1; + gateway.reportProgress(found, 4, 180); + require(progress_bar->maximum() == 180 && progress_bar->value() == 4 + && status_label->text().contains(QStringLiteral("COM3")) + && status_label->text().contains(QStringLiteral("8N1")), + "discovery progress must identify the active port and serial frame"); + + gateway.reportFound(found); + QCoreApplication::processEvents(QEventLoop::AllEvents); + const PlcSerialConfiguration selected = dialog.configuration(); + require(dialog.result() == QDialog::Accepted, + "a successful discovery must automatically accept the dialog"); + require(selected.portName == found.portName + && selected.serverAddress == found.serverAddress + && selected.baudRate == found.baudRate + && selected.dataBits == found.dataBits + && selected.parity == found.parity + && selected.stopBits == found.stopBits, + "a successful discovery must copy every detected serial parameter"); +} + +void testDiscoveryCanBeCancelled() +{ + FakePlcDiscoveryGateway gateway; + PlcSerialConfiguration initial; + initial.portName = "COM1"; + PlcConnectionDialog dialog(initial, gateway, [] {}); + QPushButton *search_button = dialog.findChild("autoSearchButton"); + QLabel *status_label = dialog.findChild("discoveryStatusLabel"); + + search_button->click(); + search_button->click(); + require(gateway.cancel_count == 1 && !gateway.discovering, + "the stop command must cancel the active discovery"); + require(search_button->isEnabled() + && search_button->text() == QStringLiteral("自动搜索") + && status_label->text() == QStringLiteral("已停止自动搜索"), + "the dialog must return to an editable state after cancellation"); +} + +void testPortRefreshRemovesUnavailableSelection() +{ + FakePlcDiscoveryGateway gateway; + PlcSerialConfiguration initial; + initial.portName = "COM_STALE_TEST_999"; + PlcConnectionDialog dialog(initial, gateway, [] {}); + QComboBox *port_combo = dialog.findChild("serialPortComboBox"); + QToolButton *refresh_button = dialog.findChild( + "refreshPortsButton"); + require(port_combo != nullptr && refresh_button != nullptr, + "PLC dialog must expose a serial port refresh command"); + require(port_combo->findText(QString::fromLatin1(initial.portName.c_str())) < 0, + "opening the dialog must not restore an unavailable saved port"); + + const QString stale_port = QStringLiteral("COM_STALE_TEST_998"); + port_combo->addItem(stale_port); + port_combo->setCurrentText(stale_port); + refresh_button->click(); + require(port_combo->findText(stale_port) < 0 + && port_combo->currentText() != stale_port, + "refreshing ports must remove an unplugged or unavailable selection"); +} + +} // namespace + +int main(int argc, char *argv[]) +{ + qputenv("QT_QPA_PLATFORM", QByteArrayLiteral("offscreen")); + QApplication application(argc, argv); + try + { + testDiscoveryFindsAndAcceptsConfiguration(); + testDiscoveryCanBeCancelled(); + testPortRefreshRemovesUnavailableSelection(); + } + catch (const std::exception &error) + { + std::cerr << "PLC connection dialog tests failed: " + << error.what() << '\n'; + return 1; + } + std::cout << "PLC connection dialog tests passed\n"; + return 0; +} diff --git a/app/tests/plc_connection_dialog_tests.pro b/app/tests/plc_connection_dialog_tests.pro new file mode 100644 index 0000000..5bd6105 --- /dev/null +++ b/app/tests/plc_connection_dialog_tests.pro @@ -0,0 +1,18 @@ +include(pri/test_defaults.pri) + +TARGET = plc_connection_dialog_tests +QT += core gui widgets serialport +CONFIG += testcase + +SOURCES += \ + plc_connection_dialog_tests.cpp \ + ../src/ui/plc_connection_dialog.cpp + +HEADERS += \ + ../src/services/plc_communication_gateway.h \ + ../src/services/plc_discovery_gateway.h \ + ../src/ui/plc_connection_dialog.h \ + support/test_support.h + +FORMS += \ + ../src/ui/plc_connection_dialog.ui diff --git a/app/tests/plc_runtime_tests.cpp b/app/tests/plc_runtime_tests.cpp index d7f8e8c..dfe7586 100644 --- a/app/tests/plc_runtime_tests.cpp +++ b/app/tests/plc_runtime_tests.cpp @@ -8,12 +8,15 @@ #include "domain/virtual_register_repository.h" #include "services/offline_simulation_service.h" #include "services/plc_communication_gateway.h" +#include "services/plc_discovery_gateway.h" #include "services/project_service.h" #include "services/runtime_mode_service.h" #include #include +#include #include +#include #include namespace { @@ -330,6 +333,72 @@ void testPlcConfigurationBoundaries() "a response timeout of 100 ms must be accepted"); } +void testPlcDiscoveryCandidatePriorityAndCoverage() +{ + PlcSerialConfiguration preferred; + preferred.portName = "COM3"; + preferred.serverAddress = 17; + preferred.baudRate = 38400; + preferred.dataBits = 7; + preferred.parity = 3; + preferred.stopBits = 2; + preferred.responseTimeoutMs = 1500; + preferred.retries = 4; + preferred.pollIntervalMs = 350; + + const std::vector candidates = + buildPlcDiscoveryCandidates( + preferred, {"COM1", "COM2", "COM3", "COM2", ""}); + require(candidates.size() == 180U, + "discovery must cover 60 serial settings on every unique available port"); + require(candidates.front().portName == "COM3" + && candidates.front().serverAddress == 17 + && candidates.front().baudRate == 38400 + && candidates.front().dataBits == 7 + && candidates.front().parity == 3 + && candidates.front().stopBits == 2, + "discovery must try the complete current configuration first"); + require(candidates.at(1).portName == "COM1" + && candidates.at(2).portName == "COM2", + "the preferred port must be first without dropping other ports"); + + std::set> unique_candidates; + bool contains_standard_8e1 = false; + bool contains_full_supported_edge = false; + for (const PlcSerialConfiguration &candidate : candidates) + { + require(candidate.serverAddress == preferred.serverAddress + && candidate.responseTimeoutMs == preferred.responseTimeoutMs + && candidate.retries == preferred.retries + && candidate.pollIntervalMs == preferred.pollIntervalMs, + "discovery must preserve the selected station and normal timing settings"); + unique_candidates.emplace( + candidate.portName, + candidate.baudRate, + candidate.dataBits, + candidate.parity, + candidate.stopBits); + contains_standard_8e1 = contains_standard_8e1 + || (candidate.portName == "COM2" + && candidate.baudRate == 9600 + && candidate.dataBits == 8 + && candidate.parity == 2 + && candidate.stopBits == 1); + contains_full_supported_edge = contains_full_supported_edge + || (candidate.portName == "COM1" + && candidate.baudRate == 115200 + && candidate.dataBits == 7 + && candidate.parity == 0 + && candidate.stopBits == 2); + } + require(unique_candidates.size() == candidates.size(), + "discovery candidates must not contain duplicate serial settings"); + require(contains_standard_8e1 && contains_full_supported_edge, + "discovery must cover common settings and every supported boundary"); + require(buildPlcDiscoveryCandidates(preferred, {}).empty(), + "discovery must stop immediately when no serial port is available"); +} + void testPlcPollQuantityBoundaries() { PlcRegisterRepository repository; @@ -374,6 +443,7 @@ int main() testRuntimeFaultRevokesOnlineReadinessAndAllowsReconnect(); testPlcCommunicationErrorClassification(); testPlcConfigurationBoundaries(); + testPlcDiscoveryCandidatePriorityAndCoverage(); testPlcPollQuantityBoundaries(); } catch (const std::exception &error) diff --git a/app/tests/pri/test_layers.pri b/app/tests/pri/test_layers.pri index f96a8d6..bb864df 100644 --- a/app/tests/pri/test_layers.pri +++ b/app/tests/pri/test_layers.pri @@ -104,10 +104,12 @@ SERVICE_OFFLINE_HEADERS = \ ../src/services/offline_simulation_service.h SERVICE_RUNTIME_SOURCES = \ - ../src/services/runtime_mode_service.cpp + ../src/services/runtime_mode_service.cpp \ + ../src/services/plc_discovery_gateway.cpp SERVICE_RUNTIME_HEADERS = \ - ../src/services/runtime_mode_service.h + ../src/services/runtime_mode_service.h \ + ../src/services/plc_discovery_gateway.h SERVICE_MONITOR_SOURCES = \ ../src/services/register_monitor_service.cpp \ @@ -126,9 +128,11 @@ INFRASTRUCTURE_JSON_HEADERS = \ INFRASTRUCTURE_PLC_SOURCES = \ ../src/infrastructure/plc_communication_error_classifier.cpp \ ../src/infrastructure/plc_register_repository.cpp \ - ../src/infrastructure/plc_communication_service.cpp + ../src/infrastructure/plc_communication_service.cpp \ + ../src/infrastructure/plc_discovery_service.cpp INFRASTRUCTURE_PLC_HEADERS = \ ../src/infrastructure/plc_communication_error_classifier.h \ ../src/infrastructure/plc_register_repository.h \ - ../src/infrastructure/plc_communication_service.h + ../src/infrastructure/plc_communication_service.h \ + ../src/infrastructure/plc_discovery_service.h diff --git a/app/tests/tests.pro b/app/tests/tests.pro index 27e09d2..028e6aa 100644 --- a/app/tests/tests.pro +++ b/app/tests/tests.pro @@ -12,6 +12,7 @@ SUBDIRS += \ register_monitor \ runtime_mode \ runtime_panel_controller \ + plc_connection_dialog \ plc_runtime domain.file = domain_tests.pro @@ -23,4 +24,5 @@ project_management.file = project_management_tests.pro register_monitor.file = register_monitor_service_tests.pro runtime_mode.file = runtime_mode_service_tests.pro runtime_panel_controller.file = runtime_panel_controller_tests.pro +plc_connection_dialog.file = plc_connection_dialog_tests.pro plc_runtime.file = plc_runtime_tests.pro