PROFINET IO Controller Stack 1.0.0
Modern C++ implementation of a PROFINET IO Controller stack
Loading...
Searching...
No Matches
cyclic.cpp
Go to the documentation of this file.
1
9
10#include "profinet/cyclic.h"
11
12#include <algorithm>
13#include <cstdio>
14#include <iostream>
15#include <ranges>
16#include <stdexcept>
17
19#include "profinet/exceptions.h"
20
21namespace profinet::cyclic
22{
23
24std::string ToString(CyclicState state)
25{
26 switch (state)
27 {
29 return "idle";
31 return "starting";
33 return "running";
35 return "stopping";
37 return "stopped";
39 return "fault";
40 }
41 return "unknown";
42}
43
45{
46 framesSent = 0;
48 framesMissed = 0;
49 framesInvalid = 0;
53 maxJitterUs = 0;
54 minCycleTimeUs = 0x7FFFFFFF;
56 lastReceiveTime = std::chrono::steady_clock::now();
59 cycleCount = 0;
60}
61
62CyclicController::CyclicController(asio::io_context& ioContext, std::string interface, MacAddress srcMac, MacAddress dstMac,
63 rt::IOCRConfig inputIocr, rt::IOCRConfig outputIocr,
64 int maxConsecutiveTimeouts, std::uint16_t etherType)
66 interface(std::move(interface)),
67 srcMac(srcMac),
68 dstMac(dstMac),
69 inputIocr(std::move(inputIocr)),
70 outputIocr(std::move(outputIocr)),
71 maxConsecutiveTimeouts(maxConsecutiveTimeouts),
72 etherType(etherType),
73 outputBuilder(this->outputIocr),
74 iocsGood(true)
75{
76 if (this->maxConsecutiveTimeouts < 0)
77 {
78 throw std::invalid_argument("max_consecutive_timeouts must be >= 0, got " +
79 std::to_string(this->maxConsecutiveTimeouts));
80 }
81
82 rxCounterStep = static_cast<std::uint32_t>(this->inputIocr.sendClockFactor) * this->inputIocr.reductionRatio;
83 txCounterStep = static_cast<std::uint32_t>(this->outputIocr.sendClockFactor) * this->outputIocr.reductionRatio;
84
86
87 // Initialize provider and consumer status to good before the first
88 // transmitted frame. Some IO devices do not start their input provider
89 // until the controller reports that it is consuming input data.
91
93}
94
96{
97 if (running)
98 {
99 Stop();
100 }
101}
102
104{
105 const double cycleMs = outputIocr.CycleTimeMs();
106 if (cycleMs < 1)
107 {
108 // Extract the formatted error message into a message
109 const std::string message = std::format(
110 "Cycle time {:.2f}ms is below 1ms - not achievable. Use a hardware-based controller for "
111 "sub-millisecond cycles.",
112 cycleMs);
113
114 throw std::invalid_argument(message);
115 }
116}
117
119{
120 const CyclicState old = state.exchange(newState);
121 if (old == newState)
122 {
123 return;
124 }
125 if (onStateChange)
126 {
127 try
128 {
129 onStateChange(old, newState);
130 }
131 catch (...)
132 {
133 }
134 }
135}
136
137void CyclicController::SetOutputData(int slot, int subslot, const Bytes& data)
138{
139 const CyclicState s = state;
141 {
142 throw std::runtime_error("Cannot set output data in " + ToString(s) + " state");
143 }
144 outputBuilder.SetData(slot, subslot, data);
145 outputBuilder.SetIops(slot, subslot, rt::IOXS_GOOD);
146}
147Bytes CyclicController::GetOutputData(const int slot, const int subslot) const
148{
149 return outputBuilder.GetData(slot, subslot);
150}
151
152std::optional<Bytes> CyclicController::GetInputData(int slot, int subslot, bool allowBad) const
153{
154 const std::lock_guard<std::mutex> lock(inputLock);
155 auto it = inputData.find({slot, subslot});
156 if (it == inputData.end() || (!allowBad && !LocalIsInputGood(slot, subslot)))
157 {
158 return std::nullopt;
159 }
160 return it->second;
161}
162
163std::optional<std::uint8_t> CyclicController::GetInputStatus(const std::uint16_t slot, const std::uint16_t subslot) const
164{
165 const std::lock_guard<std::mutex> lock(inputLock);
166
167 const auto it = inputStatus.find({slot, subslot});
168
169 if (it == inputStatus.end())
170 {
171 return std::nullopt;
172 }
173
174 return it->second;
175}
176
177bool CyclicController::IsInputGood(const std::uint16_t slot, const std::uint16_t subslot) const
178{
179 const std::lock_guard<std::mutex> lock(inputLock);
180
181 return LocalIsInputGood(slot, subslot);
182}
183
184bool CyclicController::LocalIsInputGood(const std::uint16_t slot, const std::uint16_t subslot) const
185{
186 const auto it = inputStatus.find({slot, subslot});
187
188 if (it == inputStatus.end())
189 {
190 return false;
191 }
192
193 return (it->second & rt::IOXS_DATA_STATE_GOOD) != 0;
194}
195
196void CyclicController::OnInputStatus(std::function<void(const std::uint16_t, const std::uint16_t, const std::uint8_t)> callback)
197{
198 std::lock_guard lock(callbackMutex);
199 onInputStatus = std::move(callback);
200}
201
202void CyclicController::OnInput(std::function<void(int, int, const Bytes&)> callback)
203{
204 std::lock_guard lock(callbackMutex);
205 onInputData = std::move(callback);
206}
207
208void CyclicController::OnOutput(std::function<void(int, const Bytes&)> callback)
209{
210 std::lock_guard lock(callbackMutex);
211 onOutputData = std::move(callback);
212}
213void CyclicController::OnTimeout(std::function<void()> callback)
214{
215 std::lock_guard lock(callbackMutex);
216 onTimeout = std::move(callback);
217}
218void CyclicController::OnError(std::function<void(const std::string&)> callback)
219{
220 std::lock_guard lock(callbackMutex);
221 onError = std::move(callback);
222}
223void CyclicController::OnStateChange(std::function<void(CyclicState, CyclicState)> callback)
224{
225 std::lock_guard lock(callbackMutex);
226 onStateChange = std::move(callback);
227}
228
229void CyclicController::Start(std::unique_ptr<IRawEthernetSocket> tx, std::unique_ptr<IRawEthernetSocket> rx)
230{
231 if (running)
232 {
233 return;
234 }
235
236 const CyclicState s = state;
238 {
239 throw std::runtime_error("Cannot start from " + ToString(s) + " state");
240 }
241
243 stats.Reset();
244 lastRxCycleCounter.reset();
245
246 if (tx == nullptr)
247 {
248 tx = std::make_unique<AsioEthernetSocket>(ioContext, interface, etherType);
249 }
250
251 if (rx == nullptr)
252 {
253 // The RX socket binds ALL:
254 // a socket bound to 0x8892 never sees VLAN-tagged frames unless the
255 // NIC strips the tag, and devices commonly send priority-tagged RT.
256
257 rx = std::make_unique<AsioEthernetSocket>(ioContext, interface, 0); // ALL
258 }
259
260 std::jthread txWorker;
261 std::jthread rxWorker;
262 try
263 {
264 rx->SetTimeout(std::chrono::milliseconds(1));
265
267
268 running = true;
269 txSock = std::move(tx);
270 rxSock = std::move(rx);
271
272 txWorker = std::jthread([this]
273 { TxLoop(); });
274 rxWorker = std::jthread([this]
275 { RxLoop(); });
276 }
277 catch (...)
278 {
279 running = false;
281 throw std::runtime_error("Cannot start cyclic controller.");
282 }
283 txThread = std::move(txWorker);
284 rxThread = std::move(rxWorker);
285
287}
288
290{
291 if (!running)
292 {
293 return;
294 }
295
297
298 // Signal threads to exit before sending stop frames.
299 running = false;
300
301 // Wait for TX thread first -- no concurrent socket access during stop frames.
302 if (txThread.joinable())
303 {
304 txThread.join();
305 }
306
307 // Send STOP frames after TX thread has exited.
309
310 // Wait for RX thread to finish before destroying the RX socket.
311 // The socket timeout allows Recv() to return and observe running == false.
312 if (rxThread.joinable())
313 {
314 rxThread.join();
315 }
316
317 // Close RX socket after stop response frames are all received.
318 rxSock.reset();
319
320 // Close TX socket after stop frames sent.
321 txSock.reset();
322
324}
325
327{
328 CyclicStatsSnapshot snapshot;
329
330 snapshot.framesSent = stats.framesSent.load(std::memory_order_relaxed);
331
332 snapshot.framesReceived = stats.framesReceived.load(std::memory_order_relaxed);
333
334 snapshot.framesMissed = stats.framesMissed.load(std::memory_order_relaxed);
335
336 snapshot.framesInvalid = stats.framesInvalid.load(std::memory_order_relaxed);
337
338 snapshot.framesDuplicate = stats.framesDuplicate.load(std::memory_order_relaxed);
339
340 snapshot.framesOutOfOrder = stats.framesOutOfOrder.load(std::memory_order_relaxed);
341
342 snapshot.lastCycleTimeUs = stats.lastCycleTimeUs.load(std::memory_order_relaxed);
343
344 snapshot.maxJitterUs = stats.maxJitterUs.load(std::memory_order_relaxed);
345
346 snapshot.minCycleTimeUs = stats.minCycleTimeUs.load(std::memory_order_relaxed);
347
348 snapshot.maxCycleTimeUs = stats.maxCycleTimeUs.load(std::memory_order_relaxed);
349
350 snapshot.lastReceiveTime = stats.lastReceiveTime.load(std::memory_order_relaxed);
351
352 snapshot.consecutiveTimeouts = stats.consecutiveTimeouts.load(std::memory_order_relaxed);
353
354 snapshot.cycleTimeSumUs = stats.cycleTimeSumUs.load(std::memory_order_relaxed);
355
356 snapshot.cycleCount = stats.cycleCount.load(std::memory_order_relaxed);
357
358 return snapshot;
359}
360
362{
364
366
367 stats.framesSent.fetch_add(1, std::memory_order_relaxed);
368}
369
371{
372 const double cycleTimeS = outputIocr.CycleTimeUs() / 1'000'000.0;
373 auto nextSend = std::chrono::steady_clock::now();
374 auto lastSend = nextSend;
375 bool firstFrame = true;
376
377 while (running)
378 {
379 auto now = std::chrono::steady_clock::now();
380
381 if (now >= nextSend)
382 {
383 /*
384 if (state != CyclicState::Fault)
385 {
386 outputBuilder.Swap();
387 SendOutputFrame();
388 stats.framesSent.fetch_add(1, std::memory_order_relaxed);
389 }
390 */
391 TxCycle();
392
393 if (firstFrame)
394 {
395 firstFrame = false;
396 }
397 else
398 {
399 auto actualUs =
400 static_cast<std::uint32_t>(std::chrono::duration_cast<std::chrono::microseconds>(now - lastSend)
401 .count());
402 stats.lastCycleTimeUs = actualUs;
403 const std::uint32_t target = outputIocr.CycleTimeUs();
404 const std::uint32_t jitter = actualUs > target ? actualUs - target : target - actualUs;
405 const std::uint32_t prevMax = stats.maxJitterUs.load();
406 if (jitter > prevMax)
407 {
408 stats.maxJitterUs = jitter;
409 }
410 const std::uint32_t prevMin = stats.minCycleTimeUs.load();
411 if (actualUs < prevMin)
412 {
413 stats.minCycleTimeUs = actualUs;
414 }
415 const std::uint32_t prevMaxCycle = stats.maxCycleTimeUs.load();
416 if (actualUs > prevMaxCycle)
417 {
418 stats.maxCycleTimeUs = actualUs;
419 }
420 stats.cycleTimeSumUs += actualUs;
421 stats.cycleCount += 1;
422 }
423
424 lastSend = now;
425
426 auto cycleDuration = std::chrono::duration_cast<std::chrono::steady_clock::duration>(
427 std::chrono::duration<double>(cycleTimeS));
428 nextSend += cycleDuration;
429
430 if (nextSend < now)
431 {
432 nextSend = now + cycleDuration;
433 }
434 }
435
436 auto sleepTime = nextSend - std::chrono::steady_clock::now() - std::chrono::microseconds(100);
437 if (sleepTime > std::chrono::steady_clock::duration::zero())
438 {
439 std::this_thread::sleep_for(sleepTime);
440 }
441 }
442}
443
444void CyclicController::SendOutputFrame(std::optional<std::uint8_t> dataStatusOverride)
445{
446 cycleCounter = static_cast<std::uint16_t>((cycleCounter + txCounterStep) & 0xFFFF);
447
449
450 const std::uint8_t dataStatus = dataStatusOverride.value_or(
453 // const std::uint8_t dataStatus = 0xc0;
454
457 frame.cycleCounter = cycleCounter;
458 frame.dataStatus = dataStatus;
459 // frame.dataStatus = 0x80; // Set the PROFINET DataState byte to GOOD (0x80)
460 frame.transferStatus = 0x00;
461 // frame.transferStatus = 0x80; // Set the TranferStatus byte to GOOD (0x80)
462 frame.payload = payload;
463
464 const Bytes ethFrame = rt::BuildEthernetFrame(dstMac, srcMac, frame);
465
466 try
467 {
468 if (txSock)
469 {
470 if (onOutputData)
471 {
472 onOutputData(cycleCounter, ethFrame);
473 }
474 txSock->Send(ethFrame);
475 }
476 }
477 catch (const std::exception& e)
478 {
479 if (onError)
480 {
481 onError(std::string("TX error: ") + e.what());
482 }
483 }
484}
485
487{
488 if (!txSock)
489 {
490 return;
491 }
492
493 auto stopStatus = static_cast<std::uint8_t>(rt::DATA_STATUS_VALID | rt::DATA_STATUS_STATION_OK |
495 const double cycleTimeS = outputIocr.CycleTimeUs() / 1'000'000.0;
496
497 for (int i = 0; i < STOP_FRAME_COUNT; ++i)
498 {
499 try
500 {
502 SendOutputFrame(stopStatus);
503 stats.framesSent.fetch_add(1, std::memory_order_relaxed);
504 }
505 catch (const std::exception&)
506 {
507 break;
508 }
509 if (i < STOP_FRAME_COUNT - 1)
510 {
511 std::this_thread::sleep_for(std::chrono::duration<double>(cycleTimeS));
512 }
513 }
514}
515
517{
518 const double watchdogSeconds = inputIocr.WatchdogTimeUs() / 1'000'000.0;
519 stats.lastReceiveTime = std::chrono::steady_clock::now();
520
521 while (running)
522 {
523 if (!rxSock)
524 {
525 break;
526 }
527 Bytes data;
528 try
529 {
530 data = rxSock->Recv();
531 }
532 catch (const std::exception& e)
533 {
534 if (running && onError)
535 {
536 onError(std::string("RX error: ") + e.what());
537 }
538 continue;
539 }
540
541 if (data.empty())
542 {
543 // Timeout (or socket closed returning 0 bytes) - check watchdog.
544 auto elapsed = std::chrono::duration<double>(std::chrono::steady_clock::now() - stats.lastReceiveTime.load(std::memory_order::relaxed)).count();
545 if (elapsed > watchdogSeconds)
546 {
548 stats.lastReceiveTime = std::chrono::steady_clock::now();
549 }
550 continue;
551 }
552
553 ProcessInputFrame(data);
554 }
555}
556
558{
559 stats.framesMissed.fetch_add(1, std::memory_order_relaxed);
560 const int consecutive = stats.consecutiveTimeouts.fetch_add(1, std::memory_order_relaxed) + 1;
561
563 {
564 // Set IOCS to BAD only when watchdog faulting is enabled. With
565 // max_consecutive_timeouts=0 the watchdog is monitoring-only; keep
566 // consumer status GOOD so transient RX gaps do not make the device drop
567 // an otherwise active output relationship.
569 iocsGood = false;
570 }
571
572 if (onTimeout)
573 {
574 try
575 {
576 onTimeout();
577 }
578 // NOLINTNEXTLINE(bugprone-empty-catch)
579 catch (...)
580 {
581 // proceed
582 }
583 }
584
586 {
588 if (onError)
589 {
590 onError("Communication lost: " + std::to_string(consecutive) + " consecutive watchdog timeouts");
591 }
592 }
593}
594
596{
597 // Source (6) and Destination (6) mac + (possible VLAN 2*2) + ether type (2)
598 if (data.size() < (2 * macAddressLength) + (2 * sizeof(std::uint16_t)) + sizeof(std::uint16_t))
599 {
600 // std::cout << " DROP: frame too short\n";
601 return;
602 }
603 // Parse Ethernet header; devices may send 802.1Q priority-tagged frames
604 MacAddress srcMacData{}; // dest start at 0, source at 6
605
606 std::ranges::copy(
607 data | std::views::drop(macAddressLength) | std::views::take(macAddressLength),
608 srcMacData.begin());
609
610 size_t ethOffset = SkipVlanTags(data);
611 if (data.size() < ethOffset + sizeof(std::uint16_t)) // need to have ether type
612 {
613 // std::cout << " DROP: invalid Ethernet offset\n";
614 return;
615 }
616 wire::Reader r(data);
617 r.SkipTo(ethOffset);
618 const auto ethertype = r.U16();
619 ethOffset += sizeof(std::uint16_t);
620
621 // const bool fromDevice = srcMacData == dstMac;
622 // std::cout
623 // << "Ethernet frame: "
624 // << data.size()
625 // << " bytes"
626 // << " src="
627 // << profinet::Mac2String(srcMacData)
628 // << " dst="
629 // << profinet::Mac2String(dstMac)
630 // << (fromDevice ? " [AUMA]" : " [local]")
631 // << '\n';
632
633 if (ethertype != rt::ETHERTYPE_PROFINET)
634 {
635 // std::cout << " DROP: not PROFINET RT\n";
636 return;
637 }
638
639 if (srcMacData != dstMac)
640 {
641 // std::cout << " DROP: unexpected source MAC, expected "
642 // << Mac2String(dstMac)
643 // << '\n';
644 return;
645 }
646
647 std::optional<rt::RTFrame> frameOpt;
648 try
649 {
650 // frameOpt = rt::RTFrame::FromBytes(Bytes(data.begin() + ethOffset + 3, data.end())); // ethOffset + 3 kan ook 14 of 13 zijn..
651 frameOpt = rt::RTFrame::FromBytes(Bytes(data.begin() + ethOffset, data.end())); // ethOffset + 3 kan ook 14 of 13 zijn..
652 }
653 catch (const std::exception&)
654 {
655 return;
656 }
657 rt::RTFrame& frame = *frameOpt;
658 // std::cout
659 // << " RT FrameID=0x"
660 // << std::uppercase
661 // << std::hex
662 // << frame.frameId
663 // << " expected=0x"
664 // << inputIocr.frameId
665 // << " cycleCounter=0x"
666 // << frame.cycleCounter
667 // << std::dec
668 // << '\n';
669 if (frame.frameId != inputIocr.frameId)
670 {
671 // std::cout << " DROP: FrameID mismatch\n";
672 return;
673 }
674
675 stats.lastReceiveTime = std::chrono::steady_clock::now();
676 stats.framesReceived.fetch_add(1, std::memory_order_relaxed);
678
680 {
682 }
683
684 TrackCycleCounter(frame.cycleCounter);
685 // Check validity. TransferStatus != 0 means the provider flagged a
686 // transfer problem; conformant consumers discard such frames.
687 if (!frame.IsValid() || frame.transferStatus != 0)
688 {
689 stats.framesInvalid.fetch_add(1, std::memory_order_relaxed);
690 return;
691 }
692
693 // Set IOCS to GOOD - we received valid input data. Only rewrite the
694 // buffer on a BAD->GOOD transition; set_all_iocs dirties the whole
695 // send buffer and this runs for every received frame.
696 if (!iocsGood)
697 {
699 iocsGood = true;
700 }
701
702 std::lock_guard<std::mutex> lock(inputLock);
703 // Extract data per IO object. Each submodule's payload is followed by
704 // its IOPS byte; the device sets it BAD to disown the data it is still
705 // sending, so the payload is only usable while IOPS reports GOOD.
706 std::map<std::pair<std::uint16_t, std::uint16_t>, std::uint8_t> statusEvents;
707 std::map<std::pair<std::uint16_t, std::uint16_t>, Bytes> dataEvents;
708
709 for (const auto& obj : inputIocr.objects)
710 {
711 if (!obj.offsetIOPS.has_value())
712 {
713 continue;
714 }
715 if (static_cast<std::size_t>(*obj.offsetIOPS) > frame.payload.size())
716 {
717 continue;
718 }
719 if (static_cast<std::size_t>(obj.frameOffset + obj.dataLength) > frame.payload.size())
720 {
721 continue;
722 }
723
724 std::uint8_t iops = frame.payload[*obj.offsetIOPS];
725 auto key = std::make_pair(obj.slot, obj.subslot);
726 bool wasGood = LocalIsInputGood(obj.slot, obj.subslot);
727 bool isGood = (iops & rt::IOXS_DATA_STATE_GOOD) != 0;
728 inputStatus[key] = iops;
729 const Bytes objData(frame.payload.begin() + obj.frameOffset,
730 frame.payload.begin() + obj.frameOffset + obj.dataLength);
731 // inputData[{obj.slot, obj.subslot}] = objData;
732 inputData[key] = objData;
733
734 if (isGood != wasGood)
735 {
736 statusEvents[key] = iops;
737 }
738 if (isGood)
739 {
740 dataEvents[key] = objData;
741 }
742 }
743
744 // Callbacks run outside the lock: they are application code and must not
745 // be able to stall the RX thread's next frame while holding it.
746 for (const auto& [key, iops] : statusEvents)
747 if (auto [slot, subslot] = key; true)
748 {
749 if (!(iops & rt::IOXS_DATA_STATE_GOOD))
750 {
751 /*
752 logger.warning(
753 f"Slot {slot}/{subslot}: device reports IOPS BAD (0x{iops:02X}), "
754 f"input data is no longer valid"
755 )
756 */
757 if (onError)
758 {
759 onError(std::format("Slot {}/{}: device reports IOPS BAD ({}), input data is no longer valid", slot, subslot, Hex2(iops)));
760 }
761 }
762 if (onInputStatus)
763 {
764 try
765 {
766 onInputStatus(slot, subslot, iops);
767 }
768 // NOLINTNEXTLINE(bugprone-empty-catch)
769 catch (...)
770 {
771 // continue to next -> logger.error(f"Input status callback error: {e}")
772 }
773 }
774 }
775 if (onInputData)
776 {
777 for (const auto& [key, objData] : dataEvents)
778 if (auto [slot, subslot] = key; true)
779 {
780 if (!objData.empty())
781 {
782 try
783 {
784 onInputData(slot, subslot, objData);
785 }
786 // NOLINTNEXTLINE(bugprone-empty-catch)
787 catch (...)
788 {
789 // continue to next //-->logger.error(f "Input callback error: {e}")
790 }
791 }
792 }
793 }
794}
795
796void CyclicController::TrackCycleCounter(std::uint16_t rxCounter)
797{
799 {
800 lastRxCycleCounter = rxCounter;
801 return;
802 }
803
804 const std::uint32_t step = rxCounterStep;
805 const auto expected = static_cast<std::uint16_t>((*lastRxCycleCounter + step) & 0xFFFF);
806
807 if (rxCounter == *lastRxCycleCounter)
808 {
809 stats.framesDuplicate.fetch_add(1, std::memory_order_relaxed);
810 }
811 else if (rxCounter != expected)
812 {
813 const auto forward = static_cast<std::uint16_t>((rxCounter - *lastRxCycleCounter) & 0xFFFF);
814 if (forward > 0x8000)
815 {
816 stats.framesOutOfOrder.fetch_add(1, std::memory_order_relaxed);
817 }
818 else if (step > 0)
819 {
820 const std::uint32_t gap = (forward / step) - 1;
821 if (gap > 0)
822 {
823 stats.framesMissed.fetch_add(gap, std::memory_order_relaxed);
824 }
825 }
826 lastRxCycleCounter = rxCounter;
827 }
828 else
829 {
830 lastRxCycleCounter = rxCounter;
831 }
832}
833
834} // namespace profinet::cyclic
Asio-backed platform-independent raw Ethernet transport.
void OnInputStatus(std::function< void(std::uint16_t slot, std::uint16_t subslot, std::uint8_t iops)> callback)
Register a callback for PROFINET provider status (IOPS) changes.
Definition cyclic.cpp:196
bool iocsGood
Tracks the watchdog-driven IOCS state so the RX/timeout paths only rewrite the buffer on transitions ...
Definition cyclic.h:555
void TrackCycleCounter(std::uint16_t rxCounter)
Update cycle-counter tracking stats (gaps/duplicates/reordering) for a received frame.
Definition cyclic.cpp:796
std::uint32_t rxCounterStep
Expected cycle-counter increment per received input frame.
Definition cyclic.h:527
rt::CyclicDataBuilder outputBuilder
Double-buffered builder for output cyclic data.
Definition cyclic.h:512
std::optional< Bytes > GetInputData(int slot, int subslot, bool allowBad=false) const override
Get the latest input data received from the device, if any or IOPS is BAD.
Definition cyclic.cpp:152
std::uint32_t txCounterStep
Cycle-counter increment applied per sent output frame.
Definition cyclic.h:530
std::mutex callbackMutex
Guards callback setting against concurrent access.
Definition cyclic.h:518
std::string interface
Network interface name.
Definition cyclic.h:470
void SendOutputFrame(std::optional< std::uint8_t > dataStatusOverride=std::nullopt)
Build and send one output frame.
Definition cyclic.cpp:444
std::unique_ptr< IRawEthernetSocket > rxSock
Raw socket used for receiving input frames.
Definition cyclic.h:506
void TxLoop()
TX thread body: sends output frames at the configured cycle rate.
Definition cyclic.cpp:370
asio::io_context & ioContext
Asio execution context used by the raw Ethernet transports.
Definition cyclic.h:467
std::jthread txThread
Thread sending output frames.
Definition cyclic.h:497
std::mutex inputLock
Guards inputData against concurrent access.
Definition cyclic.h:515
void TxCycle()
Swap the output buffer and transmit one cyclic output frame.
Definition cyclic.cpp:361
void SendStopFrames()
Send STOP_FRAME_COUNT output frames with ProviderRun cleared.
Definition cyclic.cpp:486
rt::IOCRConfig outputIocr
Configuration for the output (controller -> device) IOCR.
Definition cyclic.h:482
void OnError(std::function< void(const std::string &)> callback)
Register a callback invoked on TX/RX socket errors.
Definition cyclic.cpp:218
std::function< void(int, int, const std::uint8_t)> onInputStatus
Registered OnInputStatus callback, if any.
Definition cyclic.h:533
void OnStateChange(std::function< void(CyclicState oldState, CyclicState newState)> callback)
Register a callback invoked on every state transition.
Definition cyclic.cpp:223
void Stop() override
Stop gracefully.
Definition cyclic.cpp:289
~CyclicController() override
Stop (if running) and release resources.
Definition cyclic.cpp:95
void OnOutput(std::function< void(int cycleCounter, const Bytes &data)> callback)
Register a callback invoked whenever new output data is send.
Definition cyclic.cpp:208
std::atomic< bool > running
Whether the TX/RX threads should keep running.
Definition cyclic.h:494
void Transition(CyclicState newState)
Change state and invoke the OnStateChange callback if registered.
Definition cyclic.cpp:118
void CheckCycleTime()
Validate the configured cycle time, throwing if it's below 1ms.
Definition cyclic.cpp:103
void RxLoop()
RX thread body: receives and processes input frames, checking the watchdog.
Definition cyclic.cpp:516
void HandleWatchdogTimeout()
Handle a watchdog timeout: update stats and possibly transition to Fault.
Definition cyclic.cpp:557
std::jthread rxThread
Thread receiving input frames.
Definition cyclic.h:500
std::map< std::pair< std::uint16_t, std::uint16_t >, std::uint8_t > inputStatus
Last provider status (IOPS) the device sent per submodule. Payload bytes are only meaningful while th...
Definition cyclic.h:460
void SetOutputData(int slot, int subslot, const Bytes &data) override
Set output data for the next cycle.
Definition cyclic.cpp:137
std::function< void()> onTimeout
Registered OnTimeout callback, if any.
Definition cyclic.h:542
int maxConsecutiveTimeouts
Consecutive watchdog timeouts before transitioning to Fault.
Definition cyclic.h:485
rt::IOCRConfig inputIocr
Configuration for the input (device -> controller) IOCR.
Definition cyclic.h:479
MacAddress srcMac
This host's MAC address.
Definition cyclic.h:473
void OnTimeout(std::function< void()> callback)
Register a callback invoked on each watchdog timeout.
Definition cyclic.cpp:213
bool LocalIsInputGood(std::uint16_t slot, std::uint16_t subslot) const
Check whether a submodule has GOOD provider status without locking.
Definition cyclic.cpp:184
CyclicStatsSnapshot StatsSnapshot() const
Definition cyclic.cpp:326
std::atomic< CyclicState > state
Current lifecycle state.
Definition cyclic.h:488
bool IsInputGood(std::uint16_t slot, std::uint16_t subslot) const
Check whether the device currently reports GOOD provider status.
Definition cyclic.cpp:177
std::function< void(CyclicState, CyclicState)> onStateChange
Registered OnStateChange callback, if any.
Definition cyclic.h:548
std::optional< std::uint8_t > GetInputStatus(std::uint16_t slot, std::uint16_t subslot) const
Get the last provider status (IOPS) received for a submodule.
Definition cyclic.cpp:163
std::function< void(const std::string &)> onError
Registered OnError callback, if any.
Definition cyclic.h:545
void Start(std::unique_ptr< IRawEthernetSocket > tx=nullptr, std::unique_ptr< IRawEthernetSocket > rx=nullptr) override
Create TX/RX sockets and spawn the TX/RX threads.
Definition cyclic.cpp:229
std::uint16_t etherType
Current lifecycle state.
Definition cyclic.h:491
CyclicController(asio::io_context &ioContext, std::string interface, MacAddress srcMac, MacAddress dstMac, rt::IOCRConfig inputIocr, rt::IOCRConfig outputIocr, int maxConsecutiveTimeouts=DEFAULT_MAX_CONSECUTIVE_TIMEOUTS, std::uint16_t etherType=rt::ETHERTYPE_PROFINET)
Construct a controller for the given interface and IOCR configuration.
Definition cyclic.cpp:62
std::unique_ptr< IRawEthernetSocket > txSock
Raw socket used for sending output frames.
Definition cyclic.h:503
std::map< std::pair< int, int >, Bytes > inputData
Latest received data per (slot, subslot).
Definition cyclic.h:521
MacAddress dstMac
The device's MAC address.
Definition cyclic.h:476
std::function< void(int, const Bytes &)> onOutputData
Registered OnOutputData callback, if any.
Definition cyclic.h:539
std::function< void(int, int, const Bytes &)> onInputData
Registered OnInput callback, if any.
Definition cyclic.h:536
void ProcessInputFrame(const Bytes &data)
Parse and process one received input frame.
Definition cyclic.cpp:595
std::uint16_t cycleCounter
Current TX cycle counter value.
Definition cyclic.h:509
Bytes GetOutputData(int slot, int subslot) const override
Get the current output data for a slot/subslot.
Definition cyclic.cpp:147
CyclicStats stats
Communication statistics.
Definition cyclic.h:551
void OnInput(std::function< void(int slot, int subslot, const Bytes &data)> callback)
Register a callback invoked whenever new input data is received.
Definition cyclic.cpp:202
std::optional< std::uint16_t > lastRxCycleCounter
Last received cycle counter, for gap/duplicate/reorder detection.
Definition cyclic.h:524
Bytes GetData(int slot, int subslot) const
Read one object's data from the write buffer.
Definition rt.cpp:133
void SetAllIocs(std::uint8_t status=IOXS_GOOD)
Set every object's IOCS byte to the same value.
Definition rt.cpp:198
void SetData(int slot, int subslot, const Bytes &data)
Write one object's data into the write buffer.
Definition rt.cpp:116
void SetIops(int slot, int subslot, std::uint8_t status=IOXS_GOOD)
Set one object's IOPS (provider status) byte.
Definition rt.cpp:149
void Swap()
Promote the write buffer to the send buffer.
Definition rt.cpp:218
Bytes Build() const
Get the current send buffer contents.
Definition rt.cpp:228
void SetAllIops(std::uint8_t status=IOXS_GOOD)
Set every object's IOPS byte to the same value.
Definition rt.cpp:185
Lightweight cursor for parsing a big-endian byte buffer without copies.
Definition wire.h:192
void SkipTo(std::size_t offset)
Move the read position to an absolute byte offset.
Definition wire.h:343
std::uint16_t U16()
Read and consume a big-endian 16-bit value.
Definition wire.h:258
Declares the PROFINET RTC1 cyclic IO controller and process-data exchange API.
Declares exceptions and error types used by the PROFINET IO controller stack.
xmlXPathObjectPtr obj
Definition gsdml.cpp:82
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::uint8_t DATA_STATUS_STATION_OK
DataStatus bit: station health, 0=Problem, 1=OK.
Definition rt.h:89
constexpr std::uint8_t DATA_STATUS_PROVIDER_RUN
DataStatus bit: provider run state, 0=Stop, 1=Run.
Definition rt.h:87
Bytes BuildEthernetFrame(const MacAddress &dstMac, const MacAddress &srcMac, const RTFrame &rtFrame)
Build a complete Ethernet frame carrying an RT frame.
Definition rt.cpp:242
constexpr std::uint8_t DATA_STATUS_STATE
DataStatus bit: provider state, 0=Backup, 1=Primary.
Definition rt.h:79
constexpr std::uint16_t ETHERTYPE_PROFINET
EtherType used by PROFINET RT frames (0x8892).
Definition rt.h:34
constexpr std::uint8_t DATA_STATUS_VALID
DataStatus bit: data validity, 0=Invalid, 1=Valid.
Definition rt.h:83
constexpr std::uint8_t IOXS_DATA_STATE_GOOD
IOxS value: DataState is bit 7 of an IOxS byte; the lower bits carry Instance and Extension,...
Definition rt.h:99
constexpr std::uint8_t IOXS_GOOD
IOxS value: good, subslot level.
Definition rt.h:95
std::string Hex2(std::uint8_t value)
Format an 8-bit unsigned integer as a 2-digit hexadecimal string.
Definition util.cpp:49
std::array< std::uint8_t, macAddressLength > MacAddress
A 6-byte Ethernet MAC address.
Definition util.h:48
std::size_t SkipVlanTags(std::span< const std::uint8_t > frame) noexcept
Return the byte offset of the real EtherType in a raw Ethernet frame.
Definition util.cpp:70
constexpr int macAddressLength
Constant lenght of a mac address.
Definition util.h:37
std::vector< std::uint8_t > Bytes
Generic byte buffer alias used throughout the library for raw wire data.
Definition protocol.h:55
Copyable snapshot of cyclic communication statistics.
Definition cyclic.h:123
std::chrono::steady_clock::time_point lastReceiveTime
Definition cyclic.h:136
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::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::chrono::steady_clock::time_point > lastReceiveTime
Timestamp of the last received input frame (or watchdog check).
Definition cyclic.h:99
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::uint32_t CycleTimeUs() const
Compute the cycle time in microseconds.
Definition rt.h:171
std::vector< IODataObject > objects
IO data objects carried in this IOCR's cyclic frame.
Definition rt.h:163
std::uint16_t sendClockFactor
Send clock base factor (31.25us units).
Definition rt.h:148
double CycleTimeMs() const
Compute the cycle time in milliseconds.
Definition rt.h:178
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::uint32_t WatchdogTimeUs() const
Compute the watchdog timeout in microseconds.
Definition rt.h:185
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
std::unique_ptr< MockRawEthernetSocket > tx
std::unique_ptr< MockRawEthernetSocket > rx
Bytes frame
std::unique_ptr< asio::io_context > ioContext
Shared ASIO context.
Bytes payload
Definition test_dcp.cpp:392