9#include <gtest/gtest.h>
45Bytes MakeControllerOutputFrame()
48 frame.frameId = 0x8001U;
49 frame.cycleCounter = 1U;
54 frame.transferStatus = 0U;
55 frame.payload.resize(16U, 0U);
69 return static_cast<std::uint8_t
>(
frame[24] & 0x0FU);
78 return frame.size() >= 20U &&
frame[12] == 0x81U &&
frame[13] == 0x00U &&
80 static_cast<std::uint16_t
>((
static_cast<std::uint16_t
>(
frame[18]) << 8U) |
frame[19]) ==
frameId;
88 if (
frame.size() < 30U)
92 const std::uint16_t
length =
static_cast<std::uint16_t
>(
93 (
static_cast<std::uint16_t
>(
frame[28]) << 8U) |
frame[29]);
104TEST(VirtualAumaValveTests, StartsFromConfiguredGsdmlPosition)
106 const auto gsdml = MakeAumaGsdml();
107 auto iocrs = MakeTestAumaIocrs(gsdml);
109 std::move(iocrs.input), std::move(iocrs.output));
116 [](
const auto& signal)
117 { return signal.textId ==
"T_ID_ACTPOS_E2"; }),
122TEST(VirtualAumaValveTests, OpenCommandMovesValveToEndStop)
124 const auto gsdml = MakeAumaGsdml();
125 auto iocrs = MakeTestAumaIocrs(gsdml);
130 std::move(iocrs.input), std::move(iocrs.output), std::move(config));
133 controllerTx->SetTimeout(std::chrono::milliseconds(2));
135 Bytes output(7U, 0U);
143 frame.cycleCounter = 1U;
146 frame.payload.resize(16U, 0U);
147 std::copy(output.begin(), output.end(),
frame.payload.begin());
150 controllerTx->Send(ethernet);
152 ASSERT_TRUE(WaitUntil([&valve]
156 EXPECT_TRUE(WaitUntil([&valve]
159 }, std::chrono::milliseconds(500)));
165TEST(VirtualAumaValveTests, RejectsFramesFromSecondController)
167 const auto gsdml = MakeAumaGsdml();
168 auto iocrs = MakeTestAumaIocrs(gsdml);
171 std::move(iocrs.input), std::move(iocrs.output));
174 const MacAddress secondController{0x02, 0x00, 0x00, 0x00, 0x10, 0x02};
177 controllerA->SetTimeout(std::chrono::milliseconds(2));
178 controllerB->SetTimeout(std::chrono::milliseconds(2));
180 controllerA->Send(MakeControllerOutputFrame());
181 ASSERT_TRUE(WaitUntil([&valve]
195 secondFrame.
payload.resize(16U, 0U);
196 secondFrame.
payload[0] = 0x04U;
198 std::this_thread::sleep_for(std::chrono::milliseconds(20));
205TEST(VirtualAumaValveTests, SlowValveMovesButAtReducedSpeed)
207 const auto gsdml = MakeAumaGsdml();
208 auto iocrs = MakeTestAumaIocrs(gsdml);
211 std::move(iocrs.input), std::move(iocrs.output));
216 controllerTx->SetTimeout(std::chrono::milliseconds(2));
229 std::this_thread::sleep_for(std::chrono::milliseconds(120));
230 const auto position = valve.
Position();
231 EXPECT_GT(position, 0U);
232 EXPECT_LT(position, 700U);
238TEST(VirtualAumaValveTests, JammedPreventsMovement)
240 const auto gsdml = MakeAumaGsdml();
241 auto iocrs = MakeTestAumaIocrs(gsdml);
243 std::move(iocrs.input), std::move(iocrs.output));
249 controllerTx->SetTimeout(std::chrono::milliseconds(2));
263 std::this_thread::sleep_for(std::chrono::milliseconds(100));
270TEST(VirtualAumaScenarioTests, JammedKeepsIopsGood)
272 const auto gsdml = MakeAumaGsdml();
273 auto iocrs = MakeTestAumaIocrs(gsdml);
276 std::move(iocrs.input), std::move(iocrs.output));
288 .transferStatus = 0U,
293 ASSERT_TRUE(WaitUntil([&controllerRx]
295 return !controllerRx->Recv().empty();
298 controllerRx->SetTimeout(std::chrono::milliseconds(2));
299 bool goodIops =
false;
302 const Bytes received = controllerRx->Recv();
303 if (received.empty())
308 if (parsed.has_value() && parsed->frameId == 0x9000U && parsed->payload.size() > 9U)
314 EXPECT_TRUE(goodIops);
319TEST(VirtualAumaScenarioTests, BadIopsStillEmitsCyclicFramesWithBadIops)
321 const auto gsdml = MakeAumaGsdml();
322 auto iocrs = MakeTestAumaIocrs(gsdml);
325 std::move(iocrs.input), std::move(iocrs.output));
326 std::atomic<std::size_t> frames{0};
337 controllerTx->SetTimeout(std::chrono::milliseconds(2));
349 EXPECT_TRUE(WaitUntil([&frames]
351 return frames.load() >= 2U;
357TEST(VirtualAumaAlarmTests, RequestedScenariosExposeBothAlarmPaths)
359 const auto gsdml = MakeAumaGsdml();
360 auto iocrs = MakeTestAumaIocrs(gsdml);
363 std::move(iocrs.input), std::move(iocrs.output));
365 std::vector<VirtualAumaAlarmEvent> events;
366 std::mutex eventMutex;
369 std::lock_guard<std::mutex> lock(eventMutex);
370 events.push_back(event);
375 controllerTx->SetTimeout(std::chrono::milliseconds(2));
376 controllerTx->Send(MakeControllerOutputFrame());
378 ASSERT_TRUE(WaitUntil([&valve]
383 constexpr std::array<VirtualAumaScenario, 5> kRequestedScenarios{
390 for (
const auto scenario : kRequestedScenarios)
393 ASSERT_TRUE(WaitUntil([&events, &eventMutex, scenario]
395 std::lock_guard<std::mutex> lock(eventMutex);
396 return std::any_of(events.begin(), events.end(), [scenario](
const auto& event)
397 { return event.scenario == scenario; });
402 auto controllerAlarm = bus.
CreateSocket(kControllerMac);
403 controllerAlarm->SetTimeout(std::chrono::milliseconds(5));
407 Bytes notificationFrame;
408 ASSERT_TRUE(WaitUntil([&controllerAlarm, &errorFrame, ¬ificationFrame]
415 return !errorFrame.empty() && !notificationFrame.empty();
424 notificationFrame =
frame;
430 std::lock_guard<std::mutex> lock(eventMutex);
431 ASSERT_EQ(events.size(), kRequestedScenarios.size() + 1U);
432 for (
const auto& event : events)
442 ASSERT_FALSE(block.empty());
444 EXPECT_EQ(alarm.slotNumber, 1U);
445 EXPECT_EQ(alarm.subslotNumber, 1U);
446 EXPECT_EQ(alarm.moduleIdentNumber, 0x00008000U);
447 EXPECT_EQ(alarm.submoduleIdentNumber, 0x00002200U);
448 EXPECT_TRUE(alarm.channelDiagnosis);
449 EXPECT_FALSE(alarm.items.empty());
453 EXPECT_FALSE(errorFrame.empty());
454 EXPECT_FALSE(notificationFrame.empty());
459TEST(VirtualAumaAlarmTests, ThrowingAlarmCallbackDoesNotKillSimulator)
461 const auto gsdml = MakeAumaGsdml();
462 auto iocrs = MakeTestAumaIocrs(gsdml);
465 std::move(iocrs.input), std::move(iocrs.output));
468 throw std::runtime_error(
"intentional alarm callback failure");
473 controllerTx->SetTimeout(std::chrono::milliseconds(2));
474 controllerTx->Send(MakeControllerOutputFrame());
475 ASSERT_TRUE(WaitUntil([&valve]
481 std::this_thread::sleep_for(std::chrono::milliseconds(30));
487TEST(VirtualAumaScenarioTests, NoInputSuppressesDeviceInputTraffic)
489 const auto gsdml = MakeAumaGsdml();
490 auto iocrs = MakeTestAumaIocrs(gsdml);
493 std::move(iocrs.input), std::move(iocrs.output));
499 controllerTx->SetTimeout(std::chrono::milliseconds(2));
500 controllerRx->SetTimeout(std::chrono::milliseconds(2));
512 std::this_thread::sleep_for(std::chrono::milliseconds(80));
514 EXPECT_TRUE(controllerRx->Recv().empty());
519TEST(VirtualAumaScenarioTests, InvalidInputClearsDataValidity)
521 const auto gsdml = MakeAumaGsdml();
522 auto iocrs = MakeTestAumaIocrs(gsdml);
525 std::move(iocrs.input), std::move(iocrs.output));
526 std::vector<Bytes> frames;
530 std::lock_guard<std::mutex> lock(
mutex);
531 frames.push_back(
frame);
537 controllerTx->SetTimeout(std::chrono::milliseconds(2));
543 outputFrame.
payload.resize(16U, 0U);
544 outputFrame.
payload[0] = 0x01U;
547 ASSERT_TRUE(WaitUntil([&frames, &
mutex]
549 std::lock_guard<std::mutex> lock(
mutex);
550 return !frames.empty();
553 bool invalidFound =
false;
555 std::lock_guard<std::mutex> lock(
mutex);
556 for (
const auto&
frame : frames)
559 if (parsed.has_value() && parsed->frameId == 0x9000U)
565 EXPECT_TRUE(invalidFound);
570TEST(VirtualAumaScenarioTests, DuplicateInputProducesDuplicateCycleCounters)
572 const auto auma = MakeAumaGsdml();
574 auto valveIocrs = MakeTestAumaIocrs(auma);
577 std::move(valveIocrs.input), std::move(valveIocrs.output));
580 auto controllerIocrs = MakeTestAumaIocrs(auma);
584 plc.
Connect(
ioContext, bus, valve, std::move(controllerIocrs.input), std::move(controllerIocrs.output));
586 ASSERT_TRUE(WaitUntil([&plc]
589 return stats.has_value() && stats->framesReceived >= 3U;
593 ASSERT_TRUE(WaitUntil([&plc]
596 return stats.has_value() && stats->framesDuplicate > 0U;
604TEST(VirtualAumaScenarioTests, OutOfOrderInputProducesOutOfOrderCycles)
606 const auto auma = MakeAumaGsdml();
608 auto valveIocrs = MakeTestAumaIocrs(auma);
611 std::move(valveIocrs.input), std::move(valveIocrs.output));
614 auto controllerIocrs = MakeTestAumaIocrs(auma);
618 plc.
Connect(
ioContext, bus, valve, std::move(controllerIocrs.input), std::move(controllerIocrs.output));
620 ASSERT_TRUE(WaitUntil([&plc]
623 return stats.has_value() && stats->framesReceived >= 3U;
627 ASSERT_TRUE(WaitUntil([&plc]
630 return stats.has_value() && stats->framesOutOfOrder > 0U;
631 }, std::chrono::milliseconds(600)));
638TEST(VirtualAumaScenarioTests, EveryScenarioHasStableName)
652TEST(VirtualAumaValveTests, RejectsInvalidMotionConfiguration)
654 const auto gsdml = MakeAumaGsdml();
655 auto iocrs = MakeTestAumaIocrs(gsdml);
659 std::move(iocrs.input), std::move(iocrs.output),
661 std::invalid_argument);
Declares PROFINET alarm, diagnosis, maintenance, and USI payload types and parsers.
Keeps an Asio I/O context running on a dedicated worker thread.
const std::vector< IoSignal > & Sensors() const noexcept
Returns all non-reserved sensor/input signals.
Small virtual Ethernet switch suitable for deterministic tests and demos.
std::unique_ptr< InMemoryEthernetSocket > CreateSocket(MacAddress localMac)
Create one virtual raw-Ethernet endpoint.
AUMA SGx application model that behaves as a real RTC1 peer in memory.
Bytes OutputProcessData() const
Return the most recent output process-data object.
void SetScenario(VirtualAumaScenario scenario)
Set the current simulator scenario.
VirtualAumaScenario Scenario() const noexcept
Return the current scenario.
void Stop()
Stop cyclic simulation.
void OnAlarm(std::function< void(const VirtualAumaAlarmEvent &)> callback)
Register a callback for generated scenario alarm events.
const io::IoSignalCatalog & SignalCatalog() const noexcept
Return the GSDML-derived signal catalogue.
bool FaultActive() const noexcept
Return whether an application or mechanical fault is active.
void Start(InMemoryEthernetBus &bus)
Attach the device to a shared virtual Ethernet bus and start cyclic processing.
std::uint16_t TargetPosition() const noexcept
Return the current requested target position.
std::uint16_t Position() const noexcept
Return the current simulated position.
void OnFrame(std::function< void(const Bytes &)> callback)
Register a callback for every Ethernet frame emitted by the device.
Virtual Phoenix Contact AXC F 1252 controller hosting real CyclicController instances.
void Stop()
Stop all active connections.
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< cyclic::CyclicStatsSnapshot > ConnectionStats(const std::string &valveName) const
Return one connection statistics snapshot.
Declares the GSDML parser and PROFINET device-description data model.
std::optional< RTFrame > ParseEthernetFrame(const Bytes &data)
Parse a complete Ethernet frame and extract its RT frame.
constexpr std::uint8_t DATA_STATUS_STATION_OK
DataStatus bit: station health, 0=Problem, 1=OK.
constexpr std::uint8_t DATA_STATUS_PROVIDER_RUN
DataStatus bit: provider run state, 0=Stop, 1=Run.
Bytes BuildEthernetFrame(const MacAddress &dstMac, const MacAddress &srcMac, const RTFrame &rtFrame)
Build a complete Ethernet frame carrying an RT frame.
constexpr std::uint8_t DATA_STATUS_STATE
DataStatus bit: provider state, 0=Backup, 1=Primary.
constexpr std::uint16_t FRAME_ID_ALARM_LOW
Frame ID used for low-priority alarms.
constexpr std::uint8_t DATA_STATUS_VALID
DataStatus bit: data validity, 0=Invalid, 1=Valid.
constexpr std::uint8_t IOXS_GOOD
IOxS value: good, subslot level.
constexpr MacAddress kControllerMac
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.
constexpr MacAddress kValveMac
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.
@ Fault
Application fault: movement stops, IOPS becomes BAD and alarms are emitted.
@ BadIops
Input frames continue, but the input provider reports BAD IOPS.
@ DuplicateInput
Input frames repeat their previous cycle counter.
@ SlowValve
Valve travels using the configured reduced speed multiplier.
@ OutOfOrderInput
Input frames occasionally move the cycle counter backwards.
@ Normal
Fully healthy operation with normal travel speed.
@ NoInput
Input frames stop completely so the controller watchdog expires.
@ InvalidInput
Input frames remain valid but carry an invalid RTC1 DATA_STATUS.
@ Jammed
Valve accepts commands but the mechanical position does not change.
TEST(InMemoryEthernetRoutingTests, UnicastReachesOnlyMatchingEndpoint)
const char * ToString(VirtualAumaScenario scenario) noexcept
Convert a virtual AUMA scenario into its stable CLI spelling.
AlarmNotification ParseAlarmNotification(const Bytes &data)
Parse a complete AlarmNotification PDU (BlockHeader + body + items).
std::array< std::uint8_t, macAddressLength > MacAddress
A 6-byte Ethernet MAC address.
std::vector< std::uint8_t > Bytes
Generic byte buffer alias used throughout the library for raw wire data.
Declares PROFINET RT_CLASS_1 cyclic frame and IOCR data structures.
A single PROFINET Real-Time cyclic frame: Frame ID + C_SDU payload + cycle counter/status trailer.
std::uint8_t dataStatus
Data status bitmask (see DATA_STATUS_* constants).
std::uint8_t transferStatus
Transfer status (0 = OK).
std::uint16_t frameId
Frame ID identifying which IOCR this frame belongs to.
std::uint16_t cycleCounter
Cycle counter, incremented each transmission.
Bytes payload
C_SDU payload: process data plus IOPS/IOCS trailers.
Captured alarm traffic produced by a virtual AUMA scenario.
Bytes rtaErrorFrame
Complete RTA 0x0F Ethernet frame; empty when disabled.
Bytes alarmNotificationFrame
Complete RTA DATA AlarmNotification Ethernet frame; empty when disabled.
Application-level signal mapping and motion settings for a virtual AUMA SGx.
double travelUnitsPerSecond
Nominal travel speed in position units per second.
std::uint16_t initialPosition
Initial valve position in the AUMA 0..10000 application range.
std::uint8_t pduType
RTA PDU type including version nibble.
std::uint16_t frameId
Ethernet FrameID.
std::unique_ptr< asio::io_context > ioContext
Shared ASIO context.
Helpers for constructing a GSDML-derived AUMA SGx RTC1 test configuration.
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.