#include "services/runtime_mode_service.h" #include "services/offline_simulation_service.h" #include "services/project_service.h" #include "domain/active_register_repository.h" #include "domain/project_storage.h" #include "domain/register_repository.h" #include #include #include #include #include namespace { class TestProjectStorage final : public ProjectStorage { public: ProjectSaveResult save(const Project &, const std::string &) override { return {true, ProjectStorageError::None, {}}; } ProjectLoadResult load(const std::string &) override { return {false, {}, ProjectStorageError::FileReadFailed, {}}; } }; class ReadyPlcGateway final : public PlcCommunicationGateway { public: PlcCommunicationResult connectDevice(const PlcSerialConfiguration &) override { connection_state = PlcConnectionState::Connected; if (state_changed) { state_changed(); } return {true, {}}; } void disconnectDevice() override { connection_state = PlcConnectionState::Disconnected; initial_read = false; } void setPollAddresses( const std::vector &addresses) override { poll_addresses = addresses; } PlcConnectionState state() const override { return connection_state; } bool initialReadCompleted() const override { return initial_read; } PlcCommunicationError lastErrorType() const override { return PlcCommunicationError::None; } const std::string &lastError() const override { return last_error; } void setCallbacks( std::function state_callback, std::function initial_callback, std::function cache_callback, std::function error_callback) override { state_changed = std::move(state_callback); initial_read_changed = std::move(initial_callback); cache_updated = std::move(cache_callback); error_reported = std::move(error_callback); } void completeInitialRead() { initial_read = true; if (initial_read_changed) { initial_read_changed(true); } } const std::vector &pollAddresses() const { return poll_addresses; } private: PlcConnectionState connection_state = PlcConnectionState::Disconnected; bool initial_read = false; std::string last_error; std::function state_changed; std::function initial_read_changed; std::function cache_updated; std::function error_reported; std::vector poll_addresses; }; void require(bool condition, const std::string &message) { if (!condition) { throw std::runtime_error(message); } } void testModeTransitions() { // 验证服务将 PLC 首读状态与领域模式切换规则正确组合 TestProjectStorage storage; ProjectService project_service(storage); VirtualRegisterRepository virtual_repository; VirtualRegisterRepository plc_repository; ActiveRegisterRepository active_repository(virtual_repository); OfflineSimulationService simulation_service(virtual_repository); ReadyPlcGateway gateway; RuntimeModeService service(project_service, simulation_service); service.configurePlc( gateway, active_repository, virtual_repository, plc_repository); Project &project = project_service.editProject(); project.alarmDefinitions.push_back( {"alarm-m", {RegisterArea::M, 12}, AlarmCondition::MOn, 0, "M alarm"}); project.alarmDefinitions.push_back( {"alarm-d", {RegisterArea::D, 34}, AlarmCondition::DHigh, 100, "D alarm"}); project.registerComments = { {RegisterAddress{RegisterArea::M, 3999}, "只用于说明"}, {RegisterAddress{RegisterArea::D, 3999}, "只用于说明"}}; ControlLogic logic; logic.id = "poll-logic"; logic.name = "Poll logic"; LogicNode edge; edge.id = "poll-edge"; edge.config = EdgeContactNodeConfig{ RegisterAddress{RegisterArea::M, 20}, EdgeMode::Rising}; LogicNode edge_coil; edge_coil.id = "poll-edge-coil"; edge_coil.config = CoilNodeConfig{ RegisterAddress{RegisterArea::M, 21}, CoilMode::Normal}; LadderRung edge_rung; edge_rung.id = "poll-edge-rung"; edge_rung.name = "Poll edge"; edge_rung.condition = ConditionExpression::fromNode(edge); edge_rung.output = edge_coil; logic.rungs.push_back(edge_rung); LogicNode comparison; comparison.id = "poll-comparison"; comparison.config = CompareNodeConfig{ RegisterAddress{RegisterArea::D, 35}, ComparisonOperator::GreaterThan, 0}; LogicNode comparison_coil; comparison_coil.id = "poll-comparison-coil"; comparison_coil.config = CoilNodeConfig{ RegisterAddress{RegisterArea::M, 22}, CoilMode::Normal}; LadderRung comparison_rung; comparison_rung.id = "poll-comparison-rung"; comparison_rung.name = "Poll comparison"; comparison_rung.condition = ConditionExpression::fromNode(comparison); comparison_rung.output = comparison_coil; logic.rungs.push_back(comparison_rung); LogicNode timer_input; timer_input.id = "poll-timer-input"; timer_input.config = ContactNodeConfig{ RegisterAddress{RegisterArea::M, 23}, ContactMode::NormallyOpen}; LogicNode ton; ton.id = "poll-ton"; ton.config = TonNodeConfig{TimerAddress{4}, 100}; LadderRung ton_rung; ton_rung.id = "poll-ton-rung"; ton_rung.name = "Poll TON"; ton_rung.condition = ConditionExpression::fromNode(timer_input); ton_rung.output = ton; logic.rungs.push_back(ton_rung); LogicNode timer_contact; timer_contact.id = "poll-timer-contact"; timer_contact.config = TimerContactNodeConfig{ TimerAddress{4}, ContactMode::NormallyOpen}; LogicNode timer_coil; timer_coil.id = "poll-timer-coil"; timer_coil.config = CoilNodeConfig{ RegisterAddress{RegisterArea::M, 24}, CoilMode::Normal}; LadderRung timer_rung; timer_rung.id = "poll-timer-rung"; timer_rung.name = "Poll timer contact"; timer_rung.condition = ConditionExpression::fromNode(timer_contact); timer_rung.output = timer_coil; logic.rungs.push_back(timer_rung); LogicNode counter_input; counter_input.id = "poll-counter-input"; counter_input.config = ContactNodeConfig{ RegisterAddress{RegisterArea::M, 25}, ContactMode::NormallyOpen}; LogicNode counter_output; counter_output.id = "poll-counter"; counter_output.config = CounterNodeConfig{ CounterAddress{2}, CounterMode::Up, RegisterAddress{RegisterArea::D, 36}, WordOperand{ WordOperandKind::Register, RegisterAddress{RegisterArea::D, 37}, 0}, RegisterAddress{RegisterArea::M, 26}}; LadderRung counter_rung; counter_rung.id = "poll-counter-rung"; counter_rung.name = "Poll counter"; counter_rung.condition = ConditionExpression::fromNode(counter_input); counter_rung.output = counter_output; logic.rungs.push_back(counter_rung); LogicNode move_input; move_input.id = "poll-move-input"; move_input.config = ContactNodeConfig{ RegisterAddress{RegisterArea::M, 27}, ContactMode::NormallyOpen}; LogicNode move_output; move_output.id = "poll-move"; move_output.config = MoveNodeConfig{ WordOperand{ WordOperandKind::Register, RegisterAddress{RegisterArea::D, 38}, 0}, RegisterAddress{RegisterArea::D, 39}}; LadderRung move_rung; move_rung.id = "poll-move-rung"; move_rung.name = "Poll MOVE"; move_rung.condition = ConditionExpression::fromNode(move_input); move_rung.output = move_output; logic.rungs.push_back(move_rung); LogicNode add_input; add_input.id = "poll-add-input"; add_input.config = ContactNodeConfig{ RegisterAddress{RegisterArea::M, 28}, ContactMode::NormallyOpen}; LogicNode add_output; add_output.id = "poll-add"; add_output.config = ArithmeticNodeConfig{ ArithmeticOperation::Add, WordOperand{ WordOperandKind::Register, RegisterAddress{RegisterArea::D, 40}, 0}, WordOperand{ WordOperandKind::Constant, RegisterAddress{RegisterArea::D, 0}, 1}, RegisterAddress{RegisterArea::D, 41}}; LadderRung add_rung; add_rung.id = "poll-add-rung"; add_rung.name = "Poll ADD"; add_rung.condition = ConditionExpression::fromNode(add_input); add_rung.output = add_output; logic.rungs.push_back(add_rung); project.controlLogics.push_back(logic); require(service.mode() == ApplicationMode::Editing, "service must start in editing mode"); require(service.policy().allowsProjectEditing, "editing mode must allow project editing"); require(service.enterOfflineRunning().succeeded, "editing mode must enter offline running"); require(service.simulationState() == SimulationState::Running, "offline mode must start the software executor"); require(service.policy().usesVirtualRegisters, "offline running must use virtual registers"); require(service.enterOnlineRunning().error == ModeTransitionError::MustReturnToEditing, "running modes must not switch directly"); require(service.enterEditing().succeeded, "offline running must return to editing"); require(service.simulationState() == SimulationState::Stopped, "returning to editing must stop the software executor first"); require(service.enterOnlineRunning().error == ModeTransitionError::InitialPlcReadRequired, "online running must require an initial PLC read"); require(service.connectPlc( {"COM9", 1, 9600, 8, 2, 1, 1000, 2, 200}).succeeded, "PLC connection must be established before its initial read can complete"); require(gateway.pollAddresses() == std::vector({ {RegisterArea::M, 12}, {RegisterArea::M, 20}, {RegisterArea::M, 21}, {RegisterArea::M, 22}, {RegisterArea::M, 23}, {RegisterArea::M, 24}, {RegisterArea::M, 25}, {RegisterArea::M, 26}, {RegisterArea::M, 27}, {RegisterArea::M, 28}, {RegisterArea::D, 34}, {RegisterArea::D, 35}, {RegisterArea::D, 36}, {RegisterArea::D, 37}, {RegisterArea::D, 38}, {RegisterArea::D, 39}, {RegisterArea::D, 40}, {RegisterArea::D, 41}}), "all instruction M/D references must be polled while T/C resources and comments stay offline-only"); gateway.completeInitialRead(); require(service.initialPlcReadCompleted(), "service must retain the initial PLC read state"); require(service.enterOnlineRunning().succeeded, "online running must start after an initial PLC read"); require(service.simulationState() == SimulationState::Stopped, "online running must never start the software executor"); require(service.policy().usesPlcRegisters, "online running must use PLC registers"); require(!service.policy().runsLogicExecutor, "online running must keep the software executor stopped"); } } // namespace int main() { try { // 运行模式只有这一组状态机边界测试 testModeTransitions(); } catch (const std::exception &error) { std::cerr << "runtime mode service tests failed: " << error.what() << '\n'; return 1; } std::cout << "runtime mode service tests passed\n"; return 0; }