#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 "domain/virtual_register_repository.h" #include "support/test_support.h" #include #include #include #include #include namespace { using TestProjectStorage = TestSupport::InMemoryProjectStorage; 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; } PlcCommunicationResult setPollAddresses( const std::vector &addresses) override { poll_addresses = addresses; return {true, {}}; } 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 poll_cycle_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); poll_cycle_completed = std::move(poll_cycle_callback); error_reported = std::move(error_callback); } void completeInitialRead() { initial_read = true; if (initial_read_changed) { initial_read_changed(true); } } void completePollCycle() { if (poll_cycle_completed) { poll_cycle_completed(); } } 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 poll_cycle_completed; std::function error_reported; std::vector poll_addresses; }; using TestSupport::require; ConditionExpression conditionWithOutputWire( LogicNode node, const std::string &wire_id) { ConditionExpression series; series.id = wire_id + "-series"; series.kind = ConditionExpressionKind::Series; series.children = { ConditionExpression::fromNode(std::move(node)), ConditionExpression::fromWire(wire_id, 9)}; return series; } 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); OnlineLogicMonitorService online_monitor_service(plc_repository); ReadyPlcGateway gateway; RuntimeModeService service( project_service, simulation_service, online_monitor_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 = conditionWithOutputWire( edge, "poll-edge-output-wire"); 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 = conditionWithOutputWire( comparison, "poll-comparison-output-wire"); comparison_rung.output = comparison_coil; logic.rungs.push_back(comparison_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 = conditionWithOutputWire( move_input, "poll-move-output-wire"); 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 = conditionWithOutputWire( add_input, "poll-add-output-wire"); 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, 27}, {RegisterArea::M, 28}, {RegisterArea::D, 34}, {RegisterArea::D, 35}, {RegisterArea::D, 38}, {RegisterArea::D, 39}, {RegisterArea::D, 40}, {RegisterArea::D, 41}}), "all instruction M/D references must be polled while comments stay metadata-only"); gateway.completeInitialRead(); require(service.initialPlcReadCompleted(), "service must retain the initial PLC read state"); plc_repository.writeBit({RegisterArea::M, 20}, true); require(service.enterOnlineRunning().succeeded, "online running must start after an initial PLC read"); require(service.simulationState() == SimulationState::Stopped, "online running must keep the offline executor stopped"); require(service.onlineLogicMonitorService().state() == OnlineLogicMonitorState::Running, "online running must start the local read-only trace executor"); require(!plc_repository.readBit({RegisterArea::M, 21}).value, "the local trace output must not change the PLC source repository"); require(service.policy().usesPlcRegisters, "online running must use PLC registers"); require(service.policy().runsLogicExecutor, "online running must advertise the local read-only trace executor"); const std::uint64_t scan_count = service.onlineLogicMonitorService() .successfulScanCount(); gateway.completePollCycle(); require(service.onlineLogicMonitorService().successfulScanCount() == scan_count + 1U, "a completed PLC poll cycle must trigger one new local trace scan"); } } // 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; }