PROFINET IO Controller Stack 1.0.0
Modern C++ implementation of a PROFINET IO Controller stack
Loading...
Searching...
No Matches
test_cyclicMore.cpp
Go to the documentation of this file.
1
11
12#include <gtest/gtest.h>
13
14#include <array>
15#include <cstdint>
16#include <future>
17#include <memory>
18#include <optional>
19#include <stdexcept>
20#include <string>
21#include <utility>
22
25#include "profinet/cyclic.h"
26#include "profinet/rt.h"
27
28using namespace profinet;
29using namespace profinet::cyclic;
30
31namespace
32{
33
34constexpr MacAddress kControllerMac{0x00, 0x11, 0x22, 0x33, 0x44, 0x55};
35constexpr MacAddress kDeviceMac{0x02, 0x00, 0x00, 0x00, 0x00, 0x01};
36constexpr std::uint16_t kInputFrameId = 0xC001;
37constexpr std::uint16_t kOutputFrameId = 0xC000;
38constexpr std::uint16_t kProfinetEtherType = rt::ETHERTYPE_PROFINET;
39
46rt::IOCRConfig MakeConfig(std::uint16_t frameId,
47 std::uint16_t sendClockFactor = 32,
48 std::uint16_t reductionRatio = 8,
49 std::uint16_t watchdogFactor = 3)
50{
51 rt::IOCRConfig config;
52 config.frameId = frameId;
53 config.sendClockFactor = sendClockFactor;
54 config.reductionRatio = reductionRatio;
55 config.watchdogFactor = watchdogFactor;
56 config.dataLength = 40;
57 config.objects.push_back(rt::IODataObject{
58 .slot = 1,
59 .subslot = 1,
60 .frameOffset = 0,
61 .dataLength = 4,
62 .offsetIOPS = 4,
63 .offsetIOCS = 0});
64 return config;
65}
66
76Bytes BuildInputFrame(const MacAddress& source,
77 const MacAddress& destination,
78 std::uint16_t frameId = kInputFrameId,
79 std::uint16_t cycleCounter = 1,
80 std::uint8_t dataStatus = rt::RTFrame::DATA_VALID | rt::RTFrame::DATA_RUN,
81 std::uint8_t transferStatus = 0,
82 Bytes payload = {})
83{
84 if (payload.empty())
85 {
86 payload = {0xAA, 0xBB, 0xCC, 0xDD, rt::IOXS_GOOD};
87 payload.resize(40U, 0x00);
88 }
89
92 frame.cycleCounter = cycleCounter;
93 frame.dataStatus = dataStatus;
94 frame.transferStatus = transferStatus;
95 frame.payload = std::move(payload);
96
97 const Bytes rtBytes = frame.ToBytes();
98 Bytes ethernet;
99 ethernet.insert(ethernet.end(), destination.begin(), destination.end());
100 ethernet.insert(ethernet.end(), source.begin(), source.end());
101 ethernet.push_back(static_cast<std::uint8_t>((kProfinetEtherType >> 8U) & 0xFFU));
102 ethernet.push_back(static_cast<std::uint8_t>(kProfinetEtherType & 0xFFU));
103 ethernet.insert(ethernet.end(), rtBytes.begin(), rtBytes.end());
104 return ethernet;
105}
106
107} // namespace
108
109namespace profinet::cyclic
110{
111
118{
119 public:
124 {
125 controller.state = value;
126 }
127
131 static void SetRunning(CyclicController& controller, bool value)
132 {
133 controller.running = value;
134 }
135
139 static void SetTxSock(CyclicController& controller, std::unique_ptr<IRawEthernetSocket> txSock)
140 {
141 controller.txSock = std::move(txSock);
142 }
143
147 static void SetRxSock(CyclicController& controller, std::unique_ptr<IRawEthernetSocket> rxSock)
148 {
149 controller.rxSock = std::move(rxSock);
150 }
151
156 {
157 controller.Transition(value);
158 }
159
163 {
164 controller.TxCycle();
165 }
166
170 {
171 controller.TxLoop();
172 }
173
178 std::optional<std::uint8_t> dataStatusOverride = std::nullopt)
179 {
180 controller.SendOutputFrame(dataStatusOverride);
181 }
182
186 {
187 controller.SendStopFrames();
188 }
189
194 {
195 controller.ProcessInputFrame(data);
196 }
197
201 {
202 controller.HandleWatchdogTimeout();
203 }
204
208 {
209 controller.RxLoop();
210 }
211
215 static void TrackCycleCounter(CyclicController& controller, std::uint16_t cycleCounter)
216 {
217 controller.TrackCycleCounter(cycleCounter);
218 }
219
224 {
225 controller.maxConsecutiveTimeouts = value;
226 }
227
231 static void SetIocsGood(CyclicController& controller, bool value)
232 {
233 controller.iocsGood = value;
234 }
240 static void SetAllOutputIocs(CyclicController& controller, std::uint8_t status)
241 {
242 controller.outputBuilder.SetAllIocs(status);
243 }
247 [[nodiscard]] static bool IocsGood(const CyclicController& controller)
248 {
249 return controller.iocsGood;
250 }
251
256 {
257 controller.outputBuilder.Swap();
258 }
259
263 [[nodiscard]] static std::uint8_t FirstOutputByte(const CyclicController& controller)
264 {
265 return controller.outputBuilder.Build().at(0);
266 }
267
271 static void SetRxCounterStep(CyclicController& controller, std::uint32_t value)
272 {
273 controller.rxCounterStep = value;
274 }
275
281 {
282 return controller.inputIocr.objects.at(index);
283 }
284
288 {
289 controller.txSock.reset();
290 }
291
295 {
296 controller.rxSock.reset();
297 }
298
303 {
304 return controller.stats;
305 }
311 [[nodiscard]] static std::uint8_t FirstOutputIocs(const CyclicController& controller)
312 {
313 const auto& object = controller.outputIocr.objects.front();
314 const Bytes frame = controller.outputBuilder.Build();
315
316 return object.offsetIOCS.has_value() ? frame.at(*object.offsetIOCS) : 0x00;
317 }
318};
319
321class CyclicStateTests : public ::testing::Test
322{
323 protected:
326 std::unique_ptr<CyclicController> MakeController()
327 {
328 asio::io_context ioContext;
329
330 return std::make_unique<CyclicController>(
331 ioContext, "eth0", kControllerMac, kDeviceMac,
332 MakeConfig(kInputFrameId), MakeConfig(kOutputFrameId),
333 DEFAULT_MAX_CONSECUTIVE_TIMEOUTS, kProfinetEtherType);
334 }
335
337 std::unique_ptr<CyclicController> controller = MakeController();
338
340 std::unique_ptr<MockRawEthernetSocket> txSock = std::make_unique<MockRawEthernetSocket>();
341
343 std::unique_ptr<MockRawEthernetSocket> rxSock = std::make_unique<MockRawEthernetSocket>();
344
347};
348
350TEST_F(CyclicStateTests, StopDoesNotDestroyRxSocketBeforeRxThreadExits)
351{
352 auto socketState = std::make_shared<test::LifetimeTrackingSocketState>();
353
354 auto txSocket = std::make_unique<MockRawEthernetSocket>();
355 auto rxSocket = std::make_unique<test::LifetimeTrackingRawEthernetSocket>(socketState);
356 auto* rxSocketPtr = rxSocket.get();
357 auto ioContext = std::make_unique<asio::io_context>();
358 controller = std::make_unique<CyclicController>(
359 *ioContext, "eth0", kControllerMac, kDeviceMac,
360 MakeConfig(kInputFrameId), MakeConfig(kOutputFrameId),
361 DEFAULT_MAX_CONSECUTIVE_TIMEOUTS, kProfinetEtherType);
362
363 controller->Start(std::move(txSocket), std::move(rxSocket));
364
365 // Prove the RX thread is inside Recv().
366 {
367 std::unique_lock lock(socketState->mutex);
368
369 socketState->receiveEnteredCv.wait(lock, [&]
370 {
371 return socketState->receiveEntered;
372 });
373 }
374
376
377 std::atomic<bool> stopFinished;
378 std::thread stopThread(
379 [&]
380 {
381 // stopStarted.set_value();
382 controller->Stop();
383 stopFinished.store(true, std::memory_order_release);
384 });
385
386 // Give Stop() time to execute. It needs to pass txThread.join()
387 // and the sleeps inside SendStopFrames(), then block on rxThread.join().
388 // Adjust this delay if your STOP_FRAME_COUNT * cycleTimeS is larger.
389 std::this_thread::sleep_for(std::chrono::milliseconds(100));
390 EXPECT_FALSE(stopFinished.load(std::memory_order_acquire));
391
392 EXPECT_FALSE(socketState->destroyed.load(std::memory_order_acquire));
393 EXPECT_FALSE(socketState->destroyedWhileReceiving.load(std::memory_order_acquire));
394 /*
395 // Allow Recv() to return so the RX thread can terminate.
396 rxSocketPtr->ReleaseReceive();
397
398 // Recv() must now be able to finish.
399 rxSocketPtr->WaitUntilReceiveExited();
400 */
401 // Release Recv().
402 {
403 std::lock_guard lock(socketState->mutex);
404 socketState->releaseReceive = true;
405 }
406
407 socketState->receiveCv.notify_one();
408
409 // Wait for Recv() itself to finish, using state rather than the socket.
410 {
411 std::unique_lock lock(socketState->mutex);
412
413 socketState->receiveExitedCv.wait(lock, [&]
414 {
415 return socketState->receiveExited;
416 });
417 }
418 // Stop should finish its rxThread.join().
419 stopThread.join();
420
421 EXPECT_TRUE(stopFinished.load(std::memory_order_acquire));
422 EXPECT_TRUE(socketState->destroyed.load(std::memory_order_acquire));
423 EXPECT_FALSE(socketState->destroyedWhileReceiving.load(std::memory_order_acquire));
424}
425
426TEST_F(CyclicStateTests, StopIsIdempotent)
427{
428 controller->Start(std::move(txSock), std::move(rxSock));
429 controller->Stop();
430 EXPECT_NO_THROW(controller->Stop());
431}
432
433TEST_F(CyclicStateTests, CanStartAgainAfterStop)
434{
435 controller->Start(std::move(txSock), std::move(rxSock));
436 controller->Stop();
437
438 txSock = std::make_unique<MockRawEthernetSocket>();
439 rxSock = std::make_unique<MockRawEthernetSocket>();
440
441 EXPECT_NO_THROW(controller->Start(std::move(txSock), std::move(rxSock)));
442 controller->Stop();
443}
445TEST_F(CyclicStateTests, ToStringReturnsUnknownForInvalidValue)
446{
447 const auto invalidState = static_cast<CyclicState>(0xFF);
448 EXPECT_EQ(ToString(invalidState), "unknown");
449}
450
452TEST_F(CyclicStateTests, TransitionCallsCallbackForStateChange)
453{
456 controller->OnStateChange(
457 [&callbackCount = callbackCount, &oldSeen, &newSeen](CyclicState oldState, CyclicState newState)
458 {
459 ++callbackCount;
460 oldSeen = oldState;
461 newSeen = newState;
462 });
463
465
466 EXPECT_EQ(controller->State(), CyclicState::Starting);
467 EXPECT_EQ(callbackCount, 1);
468 EXPECT_EQ(oldSeen, CyclicState::Idle);
469 EXPECT_EQ(newSeen, CyclicState::Starting);
470}
471
473TEST_F(CyclicStateTests, TransitionSkipsCallbackForSameState)
474{
475 controller->OnStateChange(
476 [this](CyclicState, CyclicState)
477 {
478 ++callbackCount;
479 });
480
482
483 EXPECT_EQ(callbackCount, 0);
484}
485
487TEST_F(CyclicStateTests, TransitionSwallowsCallbackException)
488{
489 controller->OnStateChange(
491 {
492 throw std::runtime_error("state callback failure");
493 });
494
496 EXPECT_EQ(controller->State(), CyclicState::Running);
497}
498
500TEST_F(CyclicStateTests, StartRejectsInvalidStartingStates)
501{
502 const std::array<CyclicState, 3> invalidStates{
504
505 for (const CyclicState state : invalidStates)
506 {
509 EXPECT_THROW(controller->Start(std::move(txSock), std::move(rxSock)), std::runtime_error);
510 }
511}
512
514TEST_F(CyclicStateTests, StartAcceptsFaultState)
515{
518
519 controller->Start(std::move(txSock), std::move(rxSock));
520 EXPECT_TRUE(controller->IsRunning());
521 EXPECT_EQ(controller->State(), CyclicState::Running);
522
523 controller->Stop();
524 EXPECT_EQ(controller->State(), CyclicState::Stopped);
525}
526
528TEST_F(CyclicStateTests, StartAcceptsStoppedState)
529{
532
533 controller->Start(std::move(txSock), std::move(rxSock));
534 EXPECT_TRUE(controller->IsRunning());
535 EXPECT_EQ(controller->State(), CyclicState::Running);
536 controller->Stop();
537}
538
540TEST_F(CyclicStateTests, StopDoesNothingWhenNotRunning)
541{
542 EXPECT_NO_THROW(controller->Stop());
543 EXPECT_EQ(controller->State(), CyclicState::Idle);
544}
545
547TEST_F(CyclicStateTests, StartAndStopCompleteNormalLifecycle)
548{
549 controller->Start(std::move(txSock), std::move(rxSock));
550 EXPECT_TRUE(controller->IsRunning());
551 EXPECT_EQ(controller->State(), CyclicState::Running);
552
553 controller->Stop();
554 EXPECT_FALSE(controller->IsRunning());
555 EXPECT_EQ(controller->State(), CyclicState::Stopped);
556}
557
558TEST_F(CyclicStateTests, FailedStartLeavesStoppedState)
559{
560 auto* rx = rxSock.get();
561
562 EXPECT_CALL(*rx, SetTimeout(testing::_))
563 .WillOnce(testing::Throw(std::runtime_error("timeout")));
564
565 EXPECT_THROW(controller->Start(std::move(txSock), std::move(rxSock)), std::runtime_error);
566 EXPECT_FALSE(controller->IsRunning());
567 EXPECT_EQ(controller->State(), CyclicState::Fault);
568}
569
570TEST_F(CyclicStateTests, FailedStartCanBeRetried)
571{
572 auto* rx = rxSock.get();
573 EXPECT_CALL(*rx, SetTimeout(testing::_))
574 .WillOnce(testing::Throw(std::runtime_error("timeout")));
575
576 EXPECT_THROW(controller->Start(std::move(txSock), std::move(rxSock)), std::exception);
577 txSock = std::make_unique<MockRawEthernetSocket>();
578 rxSock = std::make_unique<MockRawEthernetSocket>();
579 EXPECT_NO_THROW(controller->Start(std::move(txSock), std::move(rxSock)));
580 EXPECT_EQ(controller->State(), CyclicState::Running);
581 controller->Stop();
582}
583
585class CyclicStatsTests : public ::testing::Test
586{
587 protected:
590 static std::unique_ptr<CyclicController> MakeController()
591 {
592 asio::io_context ioContext;
593
594 return std::make_unique<CyclicController>(
595 ioContext, "eth0", kControllerMac, kDeviceMac,
596 MakeConfig(kInputFrameId), MakeConfig(kOutputFrameId));
597 }
598
600 std::unique_ptr<CyclicController> controller = MakeController();
601};
602
604TEST_F(CyclicStatsTests, AverageCycleTimeReturnsIntegerAverage)
605{
606 CyclicStats stats;
607 stats.cycleTimeSumUs = 10001;
608 stats.cycleCount = 10;
609
610 EXPECT_EQ(stats.AvgCycleTimeUs(), 1000U);
611}
612
614TEST_F(CyclicStatsTests, AverageCycleTimeReturnsZeroWhenNoCycles)
615{
616 const CyclicStats stats{};
617 EXPECT_EQ(stats.AvgCycleTimeUs(), 0U);
618}
619
621TEST_F(CyclicStatsTests, SnapshotAverageCycleTimeReturnsZeroWhenNoCycles)
622{
623 const CyclicStatsSnapshot snapshot{};
624 EXPECT_EQ(snapshot.AvgCycleTimeUs(), 0U);
625}
626
628TEST_F(CyclicStatsTests, StatsSnapshotCopiesAllFields)
629{
631 stats.framesSent = 10;
632 stats.framesReceived = 9;
633 stats.framesMissed = 2;
634 stats.framesInvalid = 1;
635 stats.framesDuplicate = 3;
636 stats.framesOutOfOrder = 4;
637 stats.lastCycleTimeUs = 8000;
638 stats.maxJitterUs = 100;
639 stats.minCycleTimeUs = 7900;
640 stats.maxCycleTimeUs = 8100;
641 stats.consecutiveTimeouts = 2;
642 stats.cycleTimeSumUs = 80000;
643 stats.cycleCount = 10;
644
645 const CyclicStatsSnapshot snapshot = controller->StatsSnapshot();
646
647 EXPECT_EQ(snapshot.framesSent, 10U);
648 EXPECT_EQ(snapshot.framesReceived, 9U);
649 EXPECT_EQ(snapshot.framesMissed, 2U);
650 EXPECT_EQ(snapshot.framesInvalid, 1U);
651 EXPECT_EQ(snapshot.framesDuplicate, 3U);
652 EXPECT_EQ(snapshot.framesOutOfOrder, 4U);
653 EXPECT_EQ(snapshot.lastCycleTimeUs, 8000U);
654 EXPECT_EQ(snapshot.maxJitterUs, 100U);
655 EXPECT_EQ(snapshot.minCycleTimeUs, 7900U);
656 EXPECT_EQ(snapshot.maxCycleTimeUs, 8100U);
657 EXPECT_EQ(snapshot.consecutiveTimeouts, 2);
658 EXPECT_EQ(snapshot.cycleTimeSumUs, 80000U);
659 EXPECT_EQ(snapshot.cycleCount, 10U);
660 EXPECT_EQ(snapshot.AvgCycleTimeUs(), 8000U);
661}
662
664TEST_F(CyclicStatsTests, ResetRestoresInitialValues)
665{
666 CyclicStats stats;
667 stats.framesSent = 10;
668 stats.framesReceived = 20;
669 stats.framesMissed = 30;
670 stats.framesInvalid = 40;
671 stats.framesDuplicate = 50;
672 stats.framesOutOfOrder = 60;
673 stats.lastCycleTimeUs = 70;
674 stats.maxJitterUs = 80;
675 stats.minCycleTimeUs = 90;
676 stats.maxCycleTimeUs = 100;
677 stats.consecutiveTimeouts = 2;
678 stats.cycleTimeSumUs = 1000;
679 stats.cycleCount = 10;
680
681 stats.Reset();
682
683 EXPECT_EQ(stats.framesSent.load(), 0U);
684 EXPECT_EQ(stats.framesReceived.load(), 0U);
685 EXPECT_EQ(stats.framesMissed.load(), 0U);
686 EXPECT_EQ(stats.framesInvalid.load(), 0U);
687 EXPECT_EQ(stats.framesDuplicate.load(), 0U);
688 EXPECT_EQ(stats.framesOutOfOrder.load(), 0U);
689 EXPECT_EQ(stats.lastCycleTimeUs.load(), 0U);
690 EXPECT_EQ(stats.maxJitterUs.load(), 0U);
691 EXPECT_EQ(stats.minCycleTimeUs.load(), 0x7FFFFFFFU);
692 EXPECT_EQ(stats.maxCycleTimeUs.load(), 0U);
693 EXPECT_EQ(stats.consecutiveTimeouts.load(), 0);
694 EXPECT_EQ(stats.cycleTimeSumUs.load(), 0U);
695 EXPECT_EQ(stats.cycleCount.load(), 0U);
696 EXPECT_EQ(stats.AvgCycleTimeUs(), 0U);
697}
698
700class CyclicDataTests : public ::testing::Test
701{
702 protected:
704 void SetUp() override
705 {
706 ioContext = std::make_unique<asio::io_context>();
707 controller = std::make_unique<CyclicController>(
708 *ioContext, "eth0", kControllerMac, kDeviceMac,
709 MakeConfig(kInputFrameId), MakeConfig(kOutputFrameId));
710 }
711
714 void DeliverInput(std::uint8_t iops)
715 {
716 Bytes payload{0xAA, 0xBB, 0xCC, 0xDD, iops};
717 payload.resize(40U, 0x00);
719 *controller,
720 BuildInputFrame(kDeviceMac, kControllerMac, kInputFrameId, 1,
722 std::move(payload)));
723 }
724
726 std::unique_ptr<CyclicController> controller;
727
729 std::unique_ptr<asio::io_context> ioContext;
730};
731
733TEST_F(CyclicDataTests, GetInputDataReturnsNulloptForUnknownObject)
734{
735 EXPECT_FALSE(controller->GetInputData(99, 99).has_value());
736}
737
739TEST_F(CyclicDataTests, GetInputDataReturnsGoodInput)
740{
741 DeliverInput(rt::IOXS_GOOD);
742
743 const auto data = controller->GetInputData(1, 1);
744 ASSERT_TRUE(data.has_value());
745 EXPECT_EQ(*data, Bytes({0xAA, 0xBB, 0xCC, 0xDD}));
746}
747
749TEST_F(CyclicDataTests, GetInputDataRejectsBadInputByDefault)
750{
751 DeliverInput(rt::IOXS_BAD);
752
753 EXPECT_FALSE(controller->GetInputData(1, 1).has_value());
754 ASSERT_TRUE(controller->GetInputData(1, 1, true).has_value());
755}
756
758TEST_F(CyclicDataTests, GetInputStatusReturnsNulloptForUnknownObject)
759{
760 EXPECT_FALSE(controller->GetInputStatus(1, 1).has_value());
761}
762
764TEST_F(CyclicDataTests, GetInputStatusReturnsReceivedIops)
765{
766 DeliverInput(rt::IOXS_GOOD);
767
768 ASSERT_TRUE(controller->GetInputStatus(1, 1).has_value());
769 EXPECT_EQ(*controller->GetInputStatus(1, 1), rt::IOXS_GOOD);
770}
771
773TEST_F(CyclicDataTests, IsInputGoodReturnsFalseForUnknownObject)
774{
775 EXPECT_FALSE(controller->IsInputGood(1, 1));
776}
777
779TEST_F(CyclicDataTests, IsInputGoodReturnsTrueForGoodInput)
780{
781 DeliverInput(rt::IOXS_GOOD);
782 EXPECT_TRUE(controller->IsInputGood(1, 1));
783}
784
786TEST_F(CyclicDataTests, IsInputGoodReturnsFalseForBadInput)
787{
788 DeliverInput(rt::IOXS_BAD);
789 EXPECT_FALSE(controller->IsInputGood(1, 1));
790}
791
793TEST_F(CyclicDataTests, SetOutputDataAcceptsIdleState)
794{
795 EXPECT_NO_THROW(controller->SetOutputData(1, 1, {0xDE, 0xAD, 0xBE, 0xEF}));
796}
797
799TEST_F(CyclicDataTests, SetOutputDataRejectsStoppedState)
800{
802 EXPECT_THROW(controller->SetOutputData(1, 1, {0x01}), std::runtime_error);
803}
804
806TEST_F(CyclicDataTests, SetOutputDataRejectsFaultState)
807{
809 EXPECT_THROW(controller->SetOutputData(1, 1, {0x01}), std::runtime_error);
810}
811
813class CyclicCallbackTests : public ::testing::Test
814{
815 protected:
817 void SetUp() override
818 {
819 ioContext = std::make_unique<asio::io_context>();
820 controller = std::make_unique<CyclicController>(
821 *ioContext, "eth0", kControllerMac, kDeviceMac,
822 MakeConfig(kInputFrameId), MakeConfig(kOutputFrameId));
823 }
824
827 void DeliverInput(std::uint8_t iops)
828 {
829 Bytes payload{0xAA, 0xBB, 0xCC, 0xDD, iops};
830 payload.resize(40U, 0x00);
832 *controller,
833 BuildInputFrame(kDeviceMac, kControllerMac, kInputFrameId, 1,
835 std::move(payload)));
836 }
837
839 std::unique_ptr<CyclicController> controller;
840
842 std::unique_ptr<asio::io_context> ioContext;
843};
844
846TEST_F(CyclicCallbackTests, InputStatusCallbackRunsOnGoodTransition)
847{
848 int calls = 0;
849 controller->OnInputStatus(
850 [&calls](std::uint16_t slot, std::uint16_t subslot, std::uint8_t status)
851 {
852 ++calls;
853 EXPECT_EQ(slot, 1U);
854 EXPECT_EQ(subslot, 1U);
855 EXPECT_EQ(status, rt::IOXS_GOOD);
856 });
857
858 DeliverInput(rt::IOXS_GOOD);
859 EXPECT_EQ(calls, 1);
860}
861
863TEST_F(CyclicCallbackTests, InputStatusCallbackSkipsUnchangedStatus)
864{
865 int calls = 0;
866 controller->OnInputStatus(
867 [&calls](std::uint16_t, std::uint16_t, std::uint8_t)
868 {
869 ++calls;
870 });
871
872 DeliverInput(rt::IOXS_GOOD);
873 DeliverInput(rt::IOXS_GOOD);
874 EXPECT_EQ(calls, 1);
875}
876
878TEST_F(CyclicCallbackTests, InputStatusCallbackRunsOnBadTransition)
879{
880 std::vector<std::uint8_t> statuses;
881 int calls = 0;
882 controller->OnInputStatus(
883 [&calls, &statuses](std::uint16_t, std::uint16_t, std::uint8_t status)
884 {
885 ++calls;
886 statuses.push_back(status);
887 });
888
889 DeliverInput(rt::IOXS_GOOD);
890 DeliverInput(rt::IOXS_BAD);
891 EXPECT_EQ(calls, 2);
892 EXPECT_EQ(statuses[0], rt::IOXS_GOOD);
893 EXPECT_EQ(statuses[1], rt::IOXS_BAD);
894}
895
897TEST_F(CyclicCallbackTests, InputStatusCallbackSwallowsException)
898{
899 controller->OnInputStatus(
900 [](std::uint16_t, std::uint16_t, std::uint8_t)
901 {
902 throw std::runtime_error("status callback failure");
903 });
904
905 EXPECT_NO_THROW(DeliverInput(rt::IOXS_GOOD));
906}
907
909TEST_F(CyclicCallbackTests, InputDataCallbackRunsForGoodInput)
910{
911 int calls = 0;
912 controller->OnInput(
913 [&calls](int slot, int subslot, const Bytes& data)
914 {
915 ++calls;
916 EXPECT_EQ(slot, 1);
917 EXPECT_EQ(subslot, 1);
918 EXPECT_EQ(data, Bytes({0xAA, 0xBB, 0xCC, 0xDD}));
919 });
920
921 DeliverInput(rt::IOXS_GOOD);
922 EXPECT_EQ(calls, 1);
923}
924
926TEST_F(CyclicCallbackTests, InputDataCallbackSkipsBadInput)
927{
928 int calls = 0;
929 controller->OnInput(
930 [&calls](int, int, const Bytes&)
931 {
932 ++calls;
933 });
934
935 DeliverInput(rt::IOXS_BAD);
936 EXPECT_EQ(calls, 0);
937}
938
940TEST_F(CyclicCallbackTests, InputDataCallbackSkipsEmptyData)
941{
943 int calls = 0;
944 controller->OnInput(
945 [&calls](int, int, const Bytes&)
946 {
947 ++calls;
948 });
949
950 DeliverInput(rt::IOXS_GOOD);
951 EXPECT_EQ(calls, 0);
952}
953
955TEST_F(CyclicCallbackTests, InputDataCallbackSwallowsException)
956{
957 controller->OnInput(
958 [](int, int, const Bytes&)
959 {
960 throw std::runtime_error("data callback failure");
961 });
962
963 EXPECT_NO_THROW(DeliverInput(rt::IOXS_GOOD));
964}
965
967TEST_F(CyclicCallbackTests, TimeoutAndErrorCallbacksCanBeRegistered)
968{
969 auto ioContext = std::make_unique<asio::io_context>();
971 *ioContext, "eth0", kControllerMac, kDeviceMac,
972 MakeConfig(kInputFrameId), MakeConfig(kOutputFrameId));
973
974 bool timeoutCalled = false;
975 bool errorCalled = false;
976 controller.OnTimeout([&timeoutCalled]
977 { timeoutCalled = true; });
978 controller.OnError([&errorCalled](const std::string&)
979 { errorCalled = true; });
980
981 EXPECT_FALSE(timeoutCalled);
982 EXPECT_FALSE(errorCalled);
983}
984
986class CyclicFrameTests : public ::testing::Test
987{
988 protected:
990 void SetUp() override
991 {
992 ioContext = std::make_unique<asio::io_context>();
993
994 controller = std::make_unique<CyclicController>(
995 *ioContext, "eth0", kControllerMac, kDeviceMac,
996 MakeConfig(kInputFrameId, 1, 1), MakeConfig(kOutputFrameId));
997 }
998
1001 void ProcessCounter(std::uint16_t cycleCounter)
1002 {
1004 *controller,
1005 BuildInputFrame(kDeviceMac, kControllerMac, kInputFrameId, cycleCounter));
1006 }
1007
1009 std::unique_ptr<CyclicController> controller;
1010
1012 std::unique_ptr<asio::io_context> ioContext;
1013};
1014
1016TEST_F(CyclicFrameTests, RejectsTooShortEthernetFrame)
1017{
1019 EXPECT_EQ(controller->StatsSnapshot().framesReceived, 0U);
1020}
1021
1023TEST_F(CyclicFrameTests, RejectsUnexpectedEtherType)
1024{
1025 Bytes frame = BuildInputFrame(kDeviceMac, kControllerMac);
1026 frame[12] = 0x08;
1027 frame[13] = 0x00;
1028
1030 EXPECT_EQ(controller->StatsSnapshot().framesReceived, 0U);
1031}
1032
1034TEST_F(CyclicFrameTests, RejectsUnexpectedSourceMac)
1035{
1036 const MacAddress wrongSource = String2Mac("02:00:00:00:00:02");
1038 *controller, BuildInputFrame(wrongSource, kControllerMac));
1039
1040 EXPECT_EQ(controller->StatsSnapshot().framesReceived, 0U);
1041}
1042
1044TEST_F(CyclicFrameTests, RejectsMalformedRtFrame)
1045{
1046 Bytes frame;
1047 frame.insert(frame.end(), kControllerMac.begin(), kControllerMac.end());
1048 frame.insert(frame.end(), kDeviceMac.begin(), kDeviceMac.end());
1049 frame.push_back(0x88);
1050 frame.push_back(0x92);
1051 frame.push_back(0xC0);
1052
1054 EXPECT_EQ(controller->StatsSnapshot().framesReceived, 0U);
1055}
1056
1058TEST_F(CyclicFrameTests, RejectsUnexpectedFrameId)
1059{
1061 *controller, BuildInputFrame(kDeviceMac, kControllerMac, 0xC123));
1062 EXPECT_EQ(controller->StatsSnapshot().framesReceived, 0U);
1063}
1064
1066TEST_F(CyclicFrameTests, AcceptsVlanTaggedInputFrame)
1067{
1068 // The helper is intentionally built from the public RT serializer and then
1069 // adjusted into a VLAN frame below so SkipVlanTags() is exercised.
1070 Bytes frame = BuildInputFrame(kDeviceMac, kControllerMac);
1071 const Bytes rtBytes(frame.begin() + 14, frame.end());
1072
1073 frame.resize(12);
1074 frame.insert(frame.end(), {0x81, 0x00, 0xC0, 0x00, 0x88, 0x92});
1075 frame.insert(frame.end(), rtBytes.begin(), rtBytes.end());
1076
1078
1079 EXPECT_EQ(controller->StatsSnapshot().framesReceived, 1U);
1080 const auto data = controller->GetInputData(1, 1);
1081 ASSERT_TRUE(data.has_value());
1082 EXPECT_EQ(*data, Bytes({0xAA, 0xBB, 0xCC, 0xDD}));
1083}
1084
1086TEST_F(CyclicFrameTests, RejectsInvalidDataStatus)
1087{
1089 *controller,
1090 BuildInputFrame(kDeviceMac, kControllerMac, kInputFrameId, 1, 0x00));
1091
1092 EXPECT_EQ(controller->StatsSnapshot().framesReceived, 1U);
1093 EXPECT_EQ(controller->StatsSnapshot().framesInvalid, 1U);
1094}
1095
1097TEST_F(CyclicFrameTests, RejectsNonZeroTransferStatus)
1098{
1100 *controller,
1101 BuildInputFrame(kDeviceMac, kControllerMac, kInputFrameId, 1,
1103
1104 EXPECT_EQ(controller->StatsSnapshot().framesInvalid, 1U);
1105}
1106
1108TEST_F(CyclicFrameTests, DISABLED_ValidInputRestoresIocsGoodState)
1109{
1110 // CyclicControllerTestAccess::SetAllOutputIocs(*controller, rt::IOXS_BAD);
1113
1116
1117 // CyclicControllerTestAccess::SetIocsGood(*controller, false);
1118 CyclicControllerTestAccess::ProcessInputFrame(*controller, BuildInputFrame(kDeviceMac, kControllerMac));
1119
1121 // SetAllIocs() updates the write buffer. Swap() promotes it to the
1122 // send buffer, which is what Build() exposes.
1125}
1126
1128TEST_F(CyclicFrameTests, SkipsObjectWithInvalidIopsOffset)
1129{
1132 *controller, BuildInputFrame(kDeviceMac, kControllerMac));
1133
1134 EXPECT_EQ(controller->StatsSnapshot().framesReceived, 1U);
1135 EXPECT_FALSE(controller->GetInputStatus(1, 1).has_value());
1136}
1137
1139TEST_F(CyclicFrameTests, SkipsObjectWithInvalidDataRange)
1140{
1142 object.frameOffset = 39;
1143 object.dataLength = 4;
1144
1146 *controller, BuildInputFrame(kDeviceMac, kControllerMac));
1147
1148 EXPECT_EQ(controller->StatsSnapshot().framesReceived, 1U);
1149 EXPECT_FALSE(controller->GetInputStatus(1, 1).has_value());
1150}
1151
1153TEST_F(CyclicFrameTests, TrackCycleCounterInitializesOnFirstFrame)
1154{
1155 ProcessCounter(10);
1156 EXPECT_EQ(controller->StatsSnapshot().framesMissed, 0U);
1157}
1158
1160TEST_F(CyclicFrameTests, TrackCycleCounterCountsDuplicate)
1161{
1162 ProcessCounter(10);
1163 ProcessCounter(10);
1164 EXPECT_EQ(controller->StatsSnapshot().framesDuplicate, 1U);
1165}
1166
1168TEST_F(CyclicFrameTests, TrackCycleCounterAcceptsExpectedStep)
1169{
1170 ProcessCounter(10);
1171 ProcessCounter(11);
1172 const auto snapshot = controller->StatsSnapshot();
1173 EXPECT_EQ(snapshot.framesMissed, 0U);
1174 EXPECT_EQ(snapshot.framesDuplicate, 0U);
1175 EXPECT_EQ(snapshot.framesOutOfOrder, 0U);
1176}
1177
1179TEST_F(CyclicFrameTests, TrackCycleCounterCountsForwardGap)
1180{
1181 ProcessCounter(10);
1182 ProcessCounter(12);
1183 EXPECT_EQ(controller->StatsSnapshot().framesMissed, 1U);
1184}
1185
1187TEST_F(CyclicFrameTests, TrackCycleCounterCountsOutOfOrder)
1188{
1189 ProcessCounter(100);
1190 ProcessCounter(99);
1191 EXPECT_EQ(controller->StatsSnapshot().framesOutOfOrder, 1U);
1192}
1193
1195TEST_F(CyclicFrameTests, TrackCycleCounterHandlesZeroStep)
1196{
1200
1201 EXPECT_EQ(controller->StatsSnapshot().framesMissed, 0U);
1202 EXPECT_EQ(controller->StatsSnapshot().framesOutOfOrder, 0U);
1203}
1204
1206class CyclicWatchdogTests : public ::testing::Test
1207{
1208 protected:
1210 void SetUp() override
1211 {
1212 tx = std::make_unique<test::CyclicRawEthernetSocketStub>();
1213 rxSocket = std::make_unique<test::CyclicRawEthernetSocketStub>();
1214 rx = rxSocket.get();
1215 ioContext = std::make_unique<asio::io_context>();
1216 controller = std::make_unique<CyclicController>(
1217 *ioContext, "eth0", kControllerMac, kDeviceMac,
1218 MakeConfig(kInputFrameId, 1, 1, 1), MakeConfig(kOutputFrameId),
1219 DEFAULT_MAX_CONSECUTIVE_TIMEOUTS, kProfinetEtherType);
1222 }
1223
1225 std::unique_ptr<CyclicController> controller;
1226
1227 std::unique_ptr<test::CyclicRawEthernetSocketStub> tx = std::make_unique<test::CyclicRawEthernetSocketStub>();
1228 std::unique_ptr<test::CyclicRawEthernetSocketStub> rxSocket = std::make_unique<test::CyclicRawEthernetSocketStub>();
1229
1232
1234 std::unique_ptr<asio::io_context> ioContext;
1235};
1236
1238TEST_F(CyclicWatchdogTests, WatchdogTimeoutUpdatesStatistics)
1239{
1240 int timeoutCalls = 0;
1241 controller->OnTimeout(
1242 [&timeoutCalls]
1243 {
1244 ++timeoutCalls;
1245 });
1246
1248
1249 EXPECT_EQ(controller->StatsSnapshot().framesMissed, 1U);
1250 EXPECT_EQ(controller->StatsSnapshot().consecutiveTimeouts, 1);
1251 EXPECT_EQ(timeoutCalls, 1);
1253}
1254
1256TEST_F(CyclicWatchdogTests, MonitoringOnlyWatchdogKeepsIocsGood)
1257{
1259
1262
1264 EXPECT_EQ(controller->StatsSnapshot().consecutiveTimeouts, 2);
1265 EXPECT_EQ(controller->State(), CyclicState::Running);
1266}
1267
1269TEST_F(CyclicWatchdogTests, WatchdogFaultsAtConfiguredThreshold)
1270{
1271 std::string errorMessage;
1272 controller->OnError(
1273 [&errorMessage](const std::string& message)
1274 {
1275 errorMessage = message;
1276 });
1277
1281
1282 EXPECT_EQ(controller->State(), CyclicState::Fault);
1283 EXPECT_EQ(controller->StatsSnapshot().consecutiveTimeouts, 3);
1284 EXPECT_NE(errorMessage.find("Communication lost"), std::string::npos);
1285}
1286
1288TEST_F(CyclicWatchdogTests, WatchdogDoesNotTransitionWhenAlreadyFaulted)
1289{
1292
1294 EXPECT_EQ(controller->State(), CyclicState::Fault);
1295}
1296
1298TEST_F(CyclicWatchdogTests, TimeoutCallbackSwallowsException)
1299{
1300 controller->OnTimeout(
1301 []
1302 {
1303 throw std::runtime_error("timeout callback failure");
1304 });
1305
1307 EXPECT_EQ(controller->StatsSnapshot().framesMissed, 1U);
1308}
1309
1321
1323TEST_F(CyclicWatchdogTests, RxLoopProcessesValidFrame)
1324{
1325 rx->QueueReceive(BuildInputFrame(kDeviceMac, kControllerMac));
1326
1327 int inputCalls = 0;
1328 controller->OnInput(
1329 [this, &inputCalls](int, int, const Bytes&)
1330 {
1331 ++inputCalls;
1333 });
1334
1337
1338 EXPECT_EQ(inputCalls, 1);
1339 EXPECT_EQ(controller->StatsSnapshot().framesReceived, 1U);
1340}
1341
1343TEST_F(CyclicWatchdogTests, RxLoopStopsWithoutSocket)
1344{
1345 // Resetting the receive socket before entering RxLoop exercises its null-socket branch.
1349}
1350
1352TEST_F(CyclicWatchdogTests, RxLoopReportsSocketException)
1353{
1354 rx->SetRecvExceptionMessage("rx failure");
1355 rx->QueueReceiveException();
1356
1357 std::string errorMessage;
1358 controller->OnError(
1359 [this, &errorMessage](const std::string& message)
1360 {
1361 errorMessage = message;
1363 });
1364
1367 EXPECT_EQ(errorMessage, "RX error: rx failure");
1368}
1369
1371TEST_F(CyclicWatchdogTests, RxLoopHandlesWatchdogTimeout)
1372{
1373 rx->QueueTimeout();
1374
1375 int timeoutCalls = 0;
1376 controller->OnTimeout(
1377 [this, &timeoutCalls]
1378 {
1379 ++timeoutCalls;
1381 });
1382
1385
1386 EXPECT_EQ(timeoutCalls, 1);
1387 EXPECT_GE(controller->StatsSnapshot().framesMissed, 1U);
1388}
1389
1391class CyclicTxTests : public ::testing::Test
1392{
1393 protected:
1395 void SetUp() override
1396 {
1397 ioContext = std::make_unique<asio::io_context>();
1398 controller = std::make_unique<CyclicController>(
1399 *ioContext, "eth0", kControllerMac, kDeviceMac,
1400 MakeConfig(kInputFrameId), MakeConfig(kOutputFrameId),
1401 DEFAULT_MAX_CONSECUTIVE_TIMEOUTS, kProfinetEtherType);
1402 txSocket = std::make_unique<test::CyclicRawEthernetSocketStub>();
1403 tx = txSocket.get();
1405 }
1406
1408 std::unique_ptr<CyclicController> controller;
1409 std::unique_ptr<test::CyclicRawEthernetSocketStub> txSocket;
1410 std::unique_ptr<test::CyclicRawEthernetSocketStub> rxSocket = std::make_unique<test::CyclicRawEthernetSocketStub>();
1411
1414
1416 std::unique_ptr<asio::io_context> ioContext;
1417};
1418
1420TEST_F(CyclicTxTests, TxLoopRecordsCycleTiming)
1421{
1422 ioContext = std::make_unique<asio::io_context>();
1423 controller = std::make_unique<CyclicController>(
1424 *ioContext, "eth0", kControllerMac, kDeviceMac,
1425 MakeConfig(kInputFrameId, 8, 4), MakeConfig(kOutputFrameId, 8, 4),
1426 DEFAULT_MAX_CONSECUTIVE_TIMEOUTS, kProfinetEtherType);
1427
1428 txSocket = std::make_unique<test::CyclicRawEthernetSocketStub>();
1429 tx = txSocket.get();
1431
1432 tx->OnSend(
1433 [this]
1434 {
1435 if (tx->SentFrameCount() >= 2U)
1436 {
1437 CyclicControllerTestAccess::SetRunning(*controller, false);
1438 }
1439 });
1440
1442
1444
1445 const auto snapshot = controller->StatsSnapshot();
1446 EXPECT_EQ(snapshot.framesSent, 2U);
1447 EXPECT_EQ(snapshot.cycleCount, 1U);
1448 EXPECT_GT(snapshot.lastCycleTimeUs, 0U);
1449}
1450
1452TEST_F(CyclicTxTests, TxCycleSendsFrameAndUpdatesStatistics)
1453{
1455
1456 EXPECT_EQ(tx->SentFrameCount(), 1U);
1457 EXPECT_EQ(controller->StatsSnapshot().framesSent, 1U);
1458}
1459
1461TEST_F(CyclicTxTests, TxCycleSendsInFault)
1462{
1465 EXPECT_EQ(tx->SentFrameCount(), 1U);
1466}
1467
1469TEST_F(CyclicTxTests, SendOutputFrameWithoutSocketDoesNotThrow)
1470{
1473}
1474
1476TEST_F(CyclicTxTests, SendOutputFrameReportsSocketError)
1477{
1478 tx->SetSendThrows(true);
1479 tx->SetSendExceptionMessage("tx failure");
1480
1481 std::string errorMessage;
1482 controller->OnError(
1483 [&errorMessage](const std::string& message)
1484 {
1485 errorMessage = message;
1486 });
1488 EXPECT_EQ(errorMessage, "TX error: tx failure");
1489}
1490
1492TEST_F(CyclicTxTests, SendOutputFrameUsesDataStatusOverride)
1493{
1495
1496 ASSERT_EQ(tx->SentFrameCount(), 1U);
1497 const Bytes& ethernet = tx->SentFrame(0);
1498 const auto parsed = rt::RTFrame::FromBytes(Bytes(ethernet.begin() + 14, ethernet.end()));
1499 // ASSERT_TRUE(parsed.has_value());
1500 EXPECT_EQ(parsed.dataStatus, 0x20);
1501}
1502
1504TEST_F(CyclicTxTests, SendStopFramesGeneratesStopFrameCount)
1505{
1507
1508 EXPECT_EQ(tx->SentFrameCount(), STOP_FRAME_COUNT);
1509 EXPECT_EQ(controller->StatsSnapshot().framesSent, STOP_FRAME_COUNT);
1510}
1511
1513TEST_F(CyclicTxTests, SendStopFramesWithoutSocketDoesNothing)
1514{
1516
1519 EXPECT_EQ(controller->StatsSnapshot().framesSent, 0U);
1520}
1521
1523TEST_F(CyclicTxTests, SendStopFramesStopsAfterSendErrorCallbackThrows)
1524{
1525 tx->SetSendThrows(true);
1526 controller->OnError(
1527 [](const std::string&)
1528 {
1529 throw std::runtime_error("stop-send callback failure");
1530 });
1531
1533 EXPECT_EQ(tx->SentFrameCount(), 0U);
1534 EXPECT_EQ(controller->StatsSnapshot().framesSent, 0U);
1535}
1536
1538TEST_F(CyclicTxTests, SetOutputDataIsReflectedInTransmittedFrame)
1539{
1540 controller->SetOutputData(1, 1, {0x11, 0x22, 0x33, 0x44});
1543
1544 ASSERT_EQ(tx->SentFrameCount(), 1U);
1545
1546 const Bytes& ethernet = tx->SentFrame(0);
1547
1548 const auto parsed = rt::RTFrame::FromBytes(Bytes(ethernet.begin() + 14, ethernet.end()));
1549
1550 ASSERT_GE(parsed.payload.size(), 4U);
1551 EXPECT_EQ(parsed.payload[0], 0x11);
1552 EXPECT_EQ(parsed.payload[1], 0x22);
1553 EXPECT_EQ(parsed.payload[2], 0x33);
1554 EXPECT_EQ(parsed.payload[3], 0x44);
1555}
1556
1557TEST_F(CyclicStateTests, CallbackCanBeReplacedWhileRunning)
1558{
1559 controller->Start(std::move(txSock), std::move(rxSock));
1560 for (int i = 0; i < 1000; ++i)
1561 {
1562 controller->OnInput([i](int, int, const auto&)
1563 { (void)i; });
1564 }
1565 controller->Stop();
1566}
1567
1568TEST_F(CyclicStateTests, ClearedCallbackIsNotInvoked)
1569{
1570 int calls = 0;
1571 controller->OnInput([&](int, int, const auto&)
1572 { ++calls; });
1573 controller->OnInput(nullptr);
1575 EXPECT_EQ(calls, 0);
1576}
1577
1578TEST_F(CyclicStateTests, CallbackMayRequestStopWithoutDeadlock)
1579{
1580 controller->OnTimeout([&]
1581 { controller->Stop(); });
1582 controller->Start(std::move(txSock), std::move(rxSock));
1584 while (controller->IsRunning())
1585 {
1586 std::this_thread::sleep_for(std::chrono::milliseconds(5));
1587 }
1588
1589 EXPECT_FALSE(controller->IsRunning());
1590}
1591} // namespace profinet::cyclic
void SetUp() override
Construct the controller used by callback tests.
std::unique_ptr< asio::io_context > ioContext
Shared ASIO context.
std::unique_ptr< CyclicController > controller
Controller under test.
void DeliverInput(std::uint8_t iops)
Deliver an input frame and allow the configured callbacks to run.
Narrow private-access adapter used only by cyclic controller tests.
static void SetRunning(CyclicController &controller, bool value)
Set the controller worker running flag.
static void TxLoop(CyclicController &controller)
Invoke the private TX worker loop synchronously.
static void TrackCycleCounter(CyclicController &controller, std::uint16_t cycleCounter)
Invoke cycle-counter tracking.
static void SetTxSock(CyclicController &controller, std::unique_ptr< IRawEthernetSocket > txSock)
Set the controller txSock.
static void ResetRxSocket(CyclicController &controller)
Reset the injected RX socket.
static void SetRxCounterStep(CyclicController &controller, std::uint32_t value)
Set the RX cycle-counter step.
static void RxLoop(CyclicController &controller)
Invoke the receive loop synchronously.
static void TxCycle(CyclicController &controller)
Invoke one TX cycle synchronously.
static void HandleWatchdogTimeout(CyclicController &controller)
Invoke watchdog timeout handling.
static void SendOutputFrame(CyclicController &controller, std::optional< std::uint8_t > dataStatusOverride=std::nullopt)
Invoke output frame generation.
static bool IocsGood(const CyclicController &controller)
Read the IOCS-good bookkeeping state.
static std::uint8_t FirstOutputIocs(const CyclicController &controller)
Return the IOCS value of the first configured output object.
static void SendStopFrames(CyclicController &controller)
Invoke cyclic stop-frame generation.
static void SetAllOutputIocs(CyclicController &controller, std::uint8_t status)
Set the IOCS value for every configured output object.
static CyclicStats & Stats(CyclicController &controller)
Return the controller statistics object for targeted setup.
static void ProcessInputFrame(CyclicController &controller, const Bytes &data)
Invoke input Ethernet-frame processing.
static std::uint8_t FirstOutputByte(const CyclicController &controller)
Return the first byte of the output payload.
static void SetRxSock(CyclicController &controller, std::unique_ptr< IRawEthernetSocket > rxSock)
Set the controller rxSock.
static void SwapOutputBuilder(CyclicController &controller)
Promote the output write buffer to the send buffer.
static void Transition(CyclicController &controller, CyclicState value)
Invoke the private transition implementation.
static void SetState(CyclicController &controller, CyclicState value)
Set the controller state without starting worker threads.
static rt::IODataObject & InputObject(CyclicController &controller, std::size_t index)
Return an input IO object for malformed-frame setup.
static void SetMaxConsecutiveTimeouts(CyclicController &controller, int value)
Set the watchdog fault threshold.
static void ResetTxSocket(CyclicController &controller)
Reset the injected TX socket.
static void SetIocsGood(CyclicController &controller, bool value)
Set the IOCS-good bookkeeping state.
RT_CLASS_1 cyclic data exchange controller.
Definition cyclic.h:248
Fixture for input data and status access.
void DeliverInput(std::uint8_t iops)
Deliver one input frame with the requested IOPS status.
std::unique_ptr< asio::io_context > ioContext
Shared ASIO Context.
std::unique_ptr< CyclicController > controller
Controller under test.
void SetUp() override
Construct the controller used by data-access tests.
Fixture for input Ethernet parsing and cycle-counter tracking.
void SetUp() override
Construct a controller with a one-count RX cycle step.
std::unique_ptr< CyclicController > controller
Controller under test.
void ProcessCounter(std::uint16_t cycleCounter)
Process a valid input frame for a specific cycle counter.
std::unique_ptr< asio::io_context > ioContext
Shared ASIO context.
Fixture for state-transition and lifecycle behavior.
std::unique_ptr< CyclicController > MakeController()
Create a controller with deterministic socket injection.
std::unique_ptr< CyclicController > controller
Controller under test.
int callbackCount
Number of state callback invocations.
std::unique_ptr< MockRawEthernetSocket > txSock
Mocked Tx Socket.
std::unique_ptr< MockRawEthernetSocket > rxSock
Mocked Rx Socket.
Fixture for statistics and snapshots.
std::unique_ptr< CyclicController > controller
Controller under test.
static std::unique_ptr< CyclicController > MakeController()
Create a controller used to validate the public statistics snapshot.
Fixture for TX behavior and generated frame validation.
void SetUp() override
Construct the controller and retain its TX socket stub.
std::unique_ptr< asio::io_context > ioContext
Shared ASIO context.
std::unique_ptr< test::CyclicRawEthernetSocketStub > txSocket
std::unique_ptr< CyclicController > controller
Controller under test.
std::unique_ptr< test::CyclicRawEthernetSocketStub > rxSocket
test::CyclicRawEthernetSocketStub * tx
Injected TX socket stub.
Fixture for watchdog and RX-loop behavior.
std::unique_ptr< test::CyclicRawEthernetSocketStub > tx
std::unique_ptr< test::CyclicRawEthernetSocketStub > rxSocket
test::CyclicRawEthernetSocketStub * rx
Injected receive socket.
void SetUp() override
Construct a controller with a very short input watchdog.
std::unique_ptr< CyclicController > controller
Controller under test.
std::unique_ptr< asio::io_context > ioContext
Shared ASIO context.
Deterministic socket stub used by cyclic controller unit tests.
Declares the PROFINET RTC1 cyclic IO controller and process-data exchange API.
Deterministic raw Ethernet socket stub for cyclic-controller unit tests.
GoogleMock implementation of IRawEthernetSocket.
TEST_F(CyclicTest, CyclicStateToStringCoversAllStates)
constexpr int DEFAULT_MAX_CONSECUTIVE_TIMEOUTS
Default number of consecutive watchdog timeouts before transitioning to Fault.
Definition cyclic.h:157
std::string ToString(CyclicState state)
Human-readable name for a CyclicState value.
Definition cyclic.cpp:24
CyclicState
Lifecycle state of a CyclicController.
Definition cyclic.h:41
@ Fault
Communication failure (e.g. consecutive watchdog timeouts).
@ Running
Active cyclic data exchange.
@ Stopping
Graceful shutdown in progress (sending STOP frames).
@ Stopped
Fully stopped, threads joined.
@ Starting
Sockets created, threads launching.
@ Idle
Initial state, not yet started.
constexpr int STOP_FRAME_COUNT
Number of STOP frames sent during a graceful Stop().
Definition cyclic.h:159
constexpr std::uint8_t IOXS_BAD
IOxS value: bad.
Definition rt.h:101
constexpr std::uint16_t ETHERTYPE_PROFINET
EtherType used by PROFINET RT frames (0x8892).
Definition rt.h:34
constexpr std::uint8_t IOXS_GOOD
IOxS value: good, subslot level.
Definition rt.h:95
constexpr MacAddress kControllerMac
std::array< std::uint8_t, macAddressLength > MacAddress
A 6-byte Ethernet MAC address.
Definition util.h:48
MacAddress String2Mac(const std::string &macStr)
Parse a colon-separated MAC address string.
Definition util.cpp:122
constexpr std::uint16_t kInputFrameId
std::vector< std::uint8_t > Bytes
Generic byte buffer alias used throughout the library for raw wire data.
Definition protocol.h:55
constexpr std::uint16_t kOutputFrameId
Declares PROFINET RT_CLASS_1 cyclic frame and IOCR data structures.
Copyable snapshot of cyclic communication statistics.
Definition cyclic.h:123
std::uint64_t AvgCycleTimeUs() const
Compute average TX cycle duration.
Definition cyclic.h:145
Statistics for cyclic communication.
Definition cyclic.h:67
void Reset()
Reset all counters to their initial values.
Definition cyclic.cpp:44
std::atomic< std::uint32_t > maxJitterUs
Largest observed deviation from the target cycle time, in microseconds.
Definition cyclic.h:90
std::uint64_t AvgCycleTimeUs() const
Compute the average TX cycle duration.
Definition cyclic.h:112
std::atomic< std::uint64_t > framesReceived
Total number of input frames received.
Definition cyclic.h:72
std::atomic< std::uint64_t > framesInvalid
Total number of frames rejected for having an invalid DataStatus.
Definition cyclic.h:78
std::atomic< std::uint64_t > framesOutOfOrder
Total number of frames received out of cycle-counter order.
Definition cyclic.h:84
std::atomic< std::uint32_t > minCycleTimeUs
Smallest observed TX cycle duration, in microseconds.
Definition cyclic.h:93
std::atomic< std::uint64_t > cycleTimeSumUs
Running sum of observed TX cycle durations, in microseconds.
Definition cyclic.h:105
std::atomic< std::uint64_t > framesDuplicate
Total number of duplicate (repeated cycle counter) frames received.
Definition cyclic.h:81
std::atomic< std::uint64_t > framesMissed
Total number of watchdog timeouts (missed input frames).
Definition cyclic.h:75
std::atomic< std::uint32_t > lastCycleTimeUs
Duration of the most recently completed TX cycle, in microseconds.
Definition cyclic.h:87
std::atomic< std::uint64_t > framesSent
Total number of output frames sent.
Definition cyclic.h:69
std::atomic< std::uint64_t > cycleCount
Number of TX cycles included in CycleTimeSumUs.
Definition cyclic.h:108
std::atomic< int > consecutiveTimeouts
Current run of consecutive watchdog timeouts (resets on a good frame).
Definition cyclic.h:102
std::atomic< std::uint32_t > maxCycleTimeUs
Largest observed TX cycle duration, in microseconds.
Definition cyclic.h:96
IOCR configuration derived from AR setup: timing parameters and IO object mappings needed for cyclic ...
Definition rt.h:137
std::vector< IODataObject > objects
IO data objects carried in this IOCR's cyclic frame.
Definition rt.h:163
std::uint16_t dataLength
Total cyclic data length in bytes.
Definition rt.h:160
std::uint16_t sendClockFactor
Send clock base factor (31.25us units).
Definition rt.h:148
std::uint16_t frameId
Frame ID assigned to this IOCR.
Definition rt.h:145
std::uint16_t reductionRatio
Reduction ratio relative to the send clock.
Definition rt.h:151
std::uint16_t watchdogFactor
Watchdog factor (missed-frame tolerance before a timeout fault).
Definition rt.h:157
Single IO data object within the C_SDU: one piece of process data mapped to a slot/subslot,...
Definition rt.h:109
std::uint16_t dataLength
Length in bytes of this object's data.
Definition rt.h:120
std::optional< std::uint16_t > offsetIOPS
Byte offset of this object's IOPS (provider status) byte.
Definition rt.h:123
std::uint16_t slot
Slot number.
Definition rt.h:111
A single PROFINET Real-Time cyclic frame: Frame ID + C_SDU payload + cycle counter/status trailer.
Definition rt.h:208
static RTFrame FromBytes(const Bytes &data)
Parse an RT frame from raw bytes (after the Ethernet header).
Definition rt.cpp:14
std::uint16_t frameId
Frame ID identifying which IOCR this frame belongs to.
Definition rt.h:210
static constexpr std::uint8_t DATA_RUN
Alias for DATA_STATUS_PROVIDER_RUN, for readability at call sites.
Definition rt.h:232
static constexpr std::uint8_t DATA_VALID
Alias for DATA_STATUS_VALID, for readability at call sites.
Definition rt.h:226
std::uint16_t frameId
Ethernet FrameID.
std::unique_ptr< MockRawEthernetSocket > tx
std::unique_ptr< MockRawEthernetSocket > rx
std::unique_ptr< cyclic::CyclicController > controller
Bytes frame
std::unique_ptr< asio::io_context > ioContext
Shared ASIO context.
MockRawEthernetSocket * txSocket
MockRawEthernetSocket * rxSocket
Bytes payload
Definition test_dcp.cpp:392