PROFINET IO Controller Stack 1.0.0
Modern C++ implementation of a PROFINET IO Controller stack
Loading...
Searching...
No Matches
test_virtual_phoenix_plcMore.cpp
Go to the documentation of this file.
1
3
4#include <gtest/gtest.h>
5
6#include <chrono>
7#include <cmath>
8#include <cstdint>
9#include <limits>
10#include <stdexcept>
11#include <string>
12#include <thread>
13
17
18namespace profinet::sim
19{
20
21namespace
22{
23
27using test::WaitUntil;
28
31constexpr MacAddress TestControllerMac()
32{
33 return MacAddress{0x02, 0x00, 0x00, 0x10, 0x00, 0x01};
34}
35
38constexpr MacAddress TestValveMac()
39{
40 return MacAddress{0x02, 0x00, 0x00, 0x20, 0x00, 0x01};
41}
42
43} // namespace
44
46TEST(VirtualPhoenixPlcTests, BuildsGsdmlDerivedProcessImage)
47{
48 const auto gsdml = MakePhoenixGsdml();
49 VirtualPhoenixPlc plc("plc1", gsdml, TestControllerMac());
50
51 ASSERT_FALSE(plc.SignalCatalog().Sensors().empty());
52 ASSERT_FALSE(plc.SignalCatalog().Actuators().empty());
53 EXPECT_TRUE(plc.GetLocalInputBytes("DI4").has_value());
54 EXPECT_EQ(plc.ConnectionCount(), 0U);
55}
56
58TEST(VirtualPhoenixPlcTests, LocalInputBytesRoundTrip)
59{
60 const auto gsdml = MakePhoenixGsdml();
61 VirtualPhoenixPlc plc("plc1", gsdml, TestControllerMac());
62
63 const Bytes input{0x01U, 0x23U, 0x45U, 0x67U};
64 plc.SetLocalInputBytes("DI4", input);
65
66 ASSERT_TRUE(plc.GetLocalInputBytes("DI4").has_value());
67 EXPECT_EQ(*plc.GetLocalInputBytes("DI4"), input);
68}
69
71TEST(VirtualPhoenixPlcTests, LocalTypedValuesRoundTrip)
72{
73 const auto gsdml = MakePhoenixGsdml();
74 VirtualPhoenixPlc plc("plc1", gsdml, TestControllerMac());
75
76 plc.SetLocalInputValue("TEMPERATURE", -123.0);
77 plc.SetLocalInputValue("LEVEL", 12.5);
78 plc.SetLocalOutputValue("DO4", 305419896.0);
79 plc.SetLocalOutputValue("SETPOINT", 87.25);
80
81 ASSERT_TRUE(plc.GetLocalInputValue("TEMPERATURE").has_value());
82 ASSERT_TRUE(plc.GetLocalInputValue("LEVEL").has_value());
83 ASSERT_TRUE(plc.GetLocalOutputValue("DO4").has_value());
84 ASSERT_TRUE(plc.GetLocalOutputValue("SETPOINT").has_value());
85
86 EXPECT_DOUBLE_EQ(*plc.GetLocalInputValue("TEMPERATURE"), -123.0);
87 EXPECT_DOUBLE_EQ(*plc.GetLocalInputValue("LEVEL"), 12.5);
88 EXPECT_DOUBLE_EQ(*plc.GetLocalOutputValue("DO4"), 305419896.0);
89 EXPECT_DOUBLE_EQ(*plc.GetLocalOutputValue("SETPOINT"), 87.25);
90}
91
93TEST(VirtualPhoenixPlcTests, BitSignalsPreserveSiblingBits)
94{
95 const auto gsdml = MakePhoenixGsdml();
96 VirtualPhoenixPlc plc("plc1", gsdml, TestControllerMac());
97
98 plc.SetLocalOutputBytes("DO_STATUS_1", Bytes{0x00U});
99 plc.SetLocalOutputValue("DO_STATUS_0", 1.0);
100 plc.SetLocalOutputValue("DO_STATUS_3", 1.0);
101
102 const auto bytes = plc.GetLocalOutputBytes("DO_STATUS_0");
103 ASSERT_TRUE(bytes.has_value());
104 ASSERT_EQ(bytes->size(), 1U);
105 EXPECT_EQ((*bytes)[0], 0x09U);
106
107 plc.SetLocalOutputValue("DO_STATUS_0", 0.0);
108 const auto cleared = plc.GetLocalOutputBytes("DO_STATUS_3");
109 ASSERT_TRUE(cleared.has_value());
110 EXPECT_EQ((*cleared)[0], 0x08U);
111}
112
114TEST(VirtualPhoenixPlcTests, TypedValueValidationRejectsInvalidValues)
115{
116 const auto gsdml = MakePhoenixGsdml();
117 VirtualPhoenixPlc plc("plc1", gsdml, TestControllerMac());
118
119 EXPECT_THROW(plc.SetLocalInputValue("TEMPERATURE", 1.5), std::invalid_argument);
120 EXPECT_THROW(plc.SetLocalInputValue("TEMPERATURE", -40000.0), std::out_of_range);
121 EXPECT_THROW(plc.SetLocalInputValue("LEVEL", std::numeric_limits<double>::infinity()),
122 std::invalid_argument);
123 EXPECT_THROW(plc.SetLocalInputValue("RAW_STATUS", 1.0), std::invalid_argument);
124 EXPECT_THROW(plc.SetLocalOutputValue("DO_STATUS_0", 2.0), std::invalid_argument);
125}
126
128TEST(VirtualPhoenixPlcTests, ConnectedControllerDrivesVirtualAuma)
129{
130 const auto auma = MakeAumaGsdml();
131 const auto phoenix = MakePhoenixGsdml();
132 auto iocrs = MakeTestAumaIocrs(auma);
133
135 VirtualAumaValve valve("valve1", auma, TestValveMac(),
136 std::move(iocrs.input), std::move(iocrs.output));
137 valve.Start(bus);
138
139 VirtualPhoenixPlc plc("plc1", phoenix, TestControllerMac());
140 auto valveIocrs = MakeTestAumaIocrs(auma);
141 asio::io_context ioContext;
143
144 plc.Connect(ioContext,
145 bus,
146 valve,
147 std::move(valveIocrs.input),
148 std::move(valveIocrs.output));
149
150 ASSERT_TRUE(WaitUntil([&plc]
151 {
152 return plc.ConnectionState("valve1").has_value() &&
154 }));
155
156 plc.SetOutput("valve1", "T_ID_CMD_0", 1.0);
157
158 ASSERT_TRUE(WaitUntil([&valve]
159 {
160 return valve.Position() > 100U;
161 }));
162
163 const auto position = plc.ReadInput("valve1", "T_ID_ACTPOS_E2");
164 ASSERT_TRUE(position.has_value());
165 EXPECT_GT(*position, 0.0);
166 EXPECT_EQ(plc.ConnectionCount(), 1U);
167
168 plc.Stop();
169 valve.Stop();
170}
171
173TEST(VirtualPhoenixPlcTests, BadIopsWithholdsInputData)
174{
175 const auto auma = MakeAumaGsdml();
176 const auto phoenix = MakePhoenixGsdml();
177 auto valveIocrs = MakeTestAumaIocrs(auma);
178
180 VirtualAumaValve valve("valve1", auma, TestValveMac(),
181 std::move(valveIocrs.input), std::move(valveIocrs.output));
182 valve.Start(bus);
183
184 VirtualPhoenixPlc plc("plc1", phoenix, TestControllerMac());
185 auto controllerIocrs = MakeTestAumaIocrs(auma);
186 asio::io_context ioContext;
188
189 plc.Connect(ioContext, bus, valve,
190 std::move(controllerIocrs.input),
191 std::move(controllerIocrs.output));
192
193 ASSERT_TRUE(WaitUntil([&plc]
194 {
195 const auto stats = plc.ConnectionStats("valve1");
196 return stats.has_value() && stats->framesReceived >= 2U;
197 }));
198
200 std::this_thread::sleep_for(std::chrono::milliseconds(50));
201
202 EXPECT_FALSE(plc.ReadInput("valve1", "T_ID_ACTPOS_E2").has_value());
203 const auto stats = plc.ConnectionStats("valve1");
204 ASSERT_TRUE(stats.has_value());
205 EXPECT_GT(stats->framesReceived, 0U);
206
207 plc.Stop();
208 valve.Stop();
209}
210
212TEST(VirtualPhoenixPlcTests, NoInputTriggersControllerWatchdog)
213{
214 const auto auma = MakeAumaGsdml();
215 const auto phoenix = MakePhoenixGsdml();
216 auto valveIocrs = MakeTestAumaIocrs(auma);
217
219 VirtualAumaValve valve("valve1", auma, TestValveMac(),
220 std::move(valveIocrs.input), std::move(valveIocrs.output));
221 valve.Start(bus);
222
223 VirtualPhoenixPlc plc("plc1", phoenix, TestControllerMac());
224 auto controllerIocrs = MakeTestAumaIocrs(auma);
225 asio::io_context ioContext;
227
228 plc.Connect(ioContext, bus, valve,
229 std::move(controllerIocrs.input),
230 std::move(controllerIocrs.output),
231 2);
232
233 ASSERT_TRUE(WaitUntil([&plc]
234 {
235 const auto stats = plc.ConnectionStats("valve1");
236 return stats.has_value() && stats->framesReceived >= 2U;
237 }));
238
240
241 ASSERT_TRUE(WaitUntil([&plc]
242 {
243 const auto state = plc.ConnectionState("valve1");
244 return state.has_value() && *state == cyclic::CyclicState::Fault;
245 }, std::chrono::milliseconds(300)));
246
247 const auto stats = plc.ConnectionStats("valve1");
248 ASSERT_TRUE(stats.has_value());
249 EXPECT_GT(stats->framesMissed, 0U);
250
251 plc.Stop();
252 valve.Stop();
253}
254
256TEST(VirtualPhoenixPlcTests, DisconnectAllowsReconnect)
257{
258 const auto auma = MakeAumaGsdml();
259 const auto phoenix = MakePhoenixGsdml();
260 auto valveIocrs = MakeTestAumaIocrs(auma);
261
263
264 VirtualAumaValve valve("valve1", auma, TestValveMac(),
265 std::move(valveIocrs.input), std::move(valveIocrs.output));
266 valve.Start(bus);
267 VirtualPhoenixPlc plc("plc1", phoenix, TestControllerMac());
268
269 auto first = MakeTestAumaIocrs(auma);
270 asio::io_context ioContext;
272 plc.Connect(ioContext, bus, valve, std::move(first.input), std::move(first.output));
273 ASSERT_EQ(plc.ConnectionCount(), 1U);
274 ASSERT_TRUE(WaitUntil([&plc]
275 {
276 return plc.ConnectionState("valve1").has_value() &&
278 }));
279
280 plc.Disconnect("valve1");
281 EXPECT_EQ(plc.ConnectionCount(), 0U);
282
283 auto second = MakeTestAumaIocrs(auma);
284
285 plc.Connect(ioContext, bus, valve, std::move(second.input), std::move(second.output));
286 EXPECT_EQ(plc.ConnectionCount(), 1U);
287
288 plc.Stop();
289 valve.Stop();
290}
291
293TEST(VirtualPhoenixPlcTests, DuplicateConnectionIsRejected)
294{
295 const auto auma = MakeAumaGsdml();
296 const auto phoenix = MakePhoenixGsdml();
297 auto iocrs = MakeTestAumaIocrs(auma);
298
300 VirtualAumaValve valve("valve1", auma, TestValveMac(),
301 std::move(iocrs.input), std::move(iocrs.output));
302 valve.Start(bus);
303 VirtualPhoenixPlc plc("plc1", phoenix, TestControllerMac());
304
305 auto first = MakeTestAumaIocrs(auma);
306 asio::io_context ioContext;
308
309 plc.Connect(ioContext, bus, valve, std::move(first.input), std::move(first.output));
310 auto second = MakeTestAumaIocrs(auma);
311
312 EXPECT_THROW(plc.Connect(ioContext, bus, valve, std::move(second.input), std::move(second.output)),
313 std::invalid_argument);
314 EXPECT_EQ(plc.ConnectionCount(), 1U);
315
316 plc.Stop();
317 valve.Stop();
318}
319
320} // namespace profinet::sim
Keeps an Asio I/O context running on a dedicated worker thread.
const std::vector< IoSignal > & Actuators() const noexcept
Returns all non-reserved actuator/output signals.
Definition IOSignal.cpp:539
const std::vector< IoSignal > & Sensors() const noexcept
Returns all non-reserved sensor/input signals.
Definition IOSignal.cpp:534
Small virtual Ethernet switch suitable for deterministic tests and demos.
AUMA SGx application model that behaves as a real RTC1 peer in memory.
void SetScenario(VirtualAumaScenario scenario)
Set the current simulator scenario.
void Stop()
Stop cyclic simulation.
void Start(InMemoryEthernetBus &bus)
Attach the device to a shared virtual Ethernet bus and start cyclic processing.
std::uint16_t Position() const noexcept
Return the current simulated position.
Virtual Phoenix Contact AXC F 1252 controller hosting real CyclicController instances.
void SetLocalOutputValue(std::string_view signalTextId, double value)
Set one typed Phoenix output signal.
std::optional< double > ReadInput(const std::string &valveName, std::string_view signalTextId) const
Read a typed AUMA input signal using the production controller API.
void SetLocalOutputBytes(std::string_view signalTextId, const Bytes &data)
Set raw bytes in the local Phoenix output process image.
void SetOutput(const std::string &valveName, std::string_view signalTextId, double value)
Set a typed AUMA actuator signal.
const io::IoSignalCatalog & SignalCatalog() const noexcept
Return the Phoenix PLC's logical signal catalogue.
void Stop()
Stop all active connections.
void Disconnect(const std::string &valveName)
Disconnect a named AUMA connection.
void Connect(asio::io_context &ioContext, InMemoryEthernetBus &bus, VirtualAumaValve &valve, rt::IOCRConfig inputIocr, rt::IOCRConfig outputIocr, int maxConsecutiveTimeouts=cyclic::DEFAULT_MAX_CONSECUTIVE_TIMEOUTS)
Connect the PLC to one virtual AUMA device.
std::optional< Bytes > GetLocalInputBytes(std::string_view signalTextId) const
Read raw bytes from the local Phoenix input process image.
std::size_t ConnectionCount() const
Return the number of active AUMA connections.
std::optional< cyclic::CyclicState > ConnectionState(const std::string &valveName) const
Return one connection state.
std::optional< Bytes > GetLocalOutputBytes(std::string_view signalTextId) const
Read raw bytes from the local Phoenix output process image.
std::optional< double > GetLocalInputValue(std::string_view signalTextId) const
Read one typed Phoenix input signal.
void SetLocalInputBytes(std::string_view signalTextId, const Bytes &data)
Set raw bytes in the local Phoenix input process image.
std::optional< cyclic::CyclicStatsSnapshot > ConnectionStats(const std::string &valveName) const
Return one connection statistics snapshot.
std::optional< double > GetLocalOutputValue(std::string_view signalTextId) const
Read one typed Phoenix output signal.
void SetLocalInputValue(std::string_view signalTextId, double value)
Set one typed Phoenix input signal.
@ Fault
Communication failure (e.g. consecutive watchdog timeouts).
@ Running
Active cyclic data exchange.
gsdml::GSDMLDevice MakePhoenixGsdml()
Build a compact Phoenix GSDML model with four-byte DI/DO process data.
VirtualAumaIocrConfiguration MakeTestAumaIocrs(const gsdml::GSDMLDevice &device)
Build matching compact RTC1 IOCR configurations for the AUMA test model.
gsdml::GSDMLDevice MakeAumaGsdml()
Build a compact GSDML model using the real AUMA signal identifiers.
bool WaitUntil(const std::function< bool()> &predicate, std::chrono::milliseconds timeout=std::chrono::milliseconds(500))
Wait until a predicate becomes true or the timeout expires.
@ BadIops
Input frames continue, but the input provider reports BAD IOPS.
@ NoInput
Input frames stop completely so the controller watchdog expires.
TEST(InMemoryEthernetRoutingTests, UnicastReachesOnlyMatchingEndpoint)
std::array< std::uint8_t, macAddressLength > MacAddress
A 6-byte Ethernet MAC address.
Definition util.h:48
std::vector< std::uint8_t > Bytes
Generic byte buffer alias used throughout the library for raw wire data.
Definition protocol.h:55
std::unique_ptr< asio::io_context > ioContext
Shared ASIO context.
Stateful in-memory AUMA SGx PROFINET IO device simulator.
In-memory Phoenix Contact AXC F 1252 PLC/controller simulator.
Shared synthetic GSDML and timing helpers for virtual PROFINET tests.