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 <stdexcept>
16
17#include "profinet/exceptions.h"
18
19namespace profinet::cyclic
20{
21
22std::string ToString(CyclicState state)
23{
24 switch (state)
25 {
27 return "idle";
29 return "starting";
31 return "running";
33 return "stopping";
35 return "stopped";
37 return "fault";
38 }
39 return "unknown";
40}
41
43{
44 framesSent = 0;
46 framesMissed = 0;
47 framesInvalid = 0;
51 maxJitterUs = 0;
52 minCycleTimeUs = 0x7FFFFFFF;
54 lastReceiveTime = std::chrono::steady_clock::now();
57 cycleCount = 0;
58}
59
60CyclicController::CyclicController(std::string interface, MacAddress srcMac, MacAddress dstMac,
61 rt::IOCRConfig inputIocr, rt::IOCRConfig outputIocr,
62 int maxConsecutiveTimeouts, std::uint16_t etherType)
63 : interface(std::move(interface)),
64 srcMac(srcMac),
65 dstMac(dstMac),
66 inputIocr(std::move(inputIocr)),
67 outputIocr(std::move(outputIocr)),
68 maxConsecutiveTimeouts(maxConsecutiveTimeouts),
69 etherType(etherType),
70 outputBuilder(this->outputIocr),
71 iocsGood(true)
72{
73 if (this->maxConsecutiveTimeouts < 0)
74 {
75 throw std::invalid_argument("max_consecutive_timeouts must be >= 0, got " +
76 std::to_string(this->maxConsecutiveTimeouts));
77 }
78
79 rxCounterStep = static_cast<std::uint32_t>(this->inputIocr.sendClockFactor) * this->inputIocr.reductionRatio;
80 txCounterStep = static_cast<std::uint32_t>(this->outputIocr.sendClockFactor) * this->outputIocr.reductionRatio;
81
83
84 // Initialize provider and consumer status to good before the first
85 // transmitted frame. Some IO devices do not start their input provider
86 // until the controller reports that it is consuming input data.
88
90}
91
93{
94 if (running)
95 {
96 Stop();
97 }
98}
99
101{
102 const double cycleMs = outputIocr.CycleTimeMs();
103 if (cycleMs < 1)
104 {
105 // Extract the formatted error message into a message
106 const std::string message = std::format(
107 "Cycle time {:.2f}ms is below 1ms - not achievable. Use a hardware-based controller for "
108 "sub-millisecond cycles.",
109 cycleMs);
110
111 throw std::invalid_argument(message);
112 }
113 // Cycle times below PYTHON_MIN_CYCLE_MS were flagged in the Python
114 // original due to GIL/interpreter overhead; kept as a soft warning
115 // here for parity, though this port has no equivalent hard limit.
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}
147
148std::optional<Bytes> CyclicController::GetInputData(int slot, int subslot, bool allowBad) const
149{
150 const std::lock_guard<std::mutex> lock(inputLock);
151 auto it = inputData.find({slot, subslot});
152 if (it == inputData.end() || (!allowBad && !LocalIsInputGood(slot, subslot)))
153 {
154 return std::nullopt;
155 }
156 return it->second;
157}
158
159std::optional<std::uint8_t> CyclicController::GetInputStatus(const std::uint16_t slot, const std::uint16_t subslot) const
160{
161 const std::lock_guard<std::mutex> lock(inputLock);
162
163 const auto it = inputStatus.find({slot, subslot});
164
165 if (it == inputStatus.end())
166 {
167 return std::nullopt;
168 }
169
170 return it->second;
171}
172
173bool CyclicController::IsInputGood(const std::uint16_t slot, const std::uint16_t subslot) const
174{
175 const std::lock_guard<std::mutex> lock(inputLock);
176
177 return LocalIsInputGood(slot, subslot);
178}
179
180bool CyclicController::LocalIsInputGood(const std::uint16_t slot, const std::uint16_t subslot) const
181{
182 const auto it = inputStatus.find({slot, subslot});
183
184 if (it == inputStatus.end())
185 {
186 return false;
187 }
188
189 return (it->second & rt::IOXS_DATA_STATE_GOOD) != 0;
190}
191
192void CyclicController::OnInputStatus(std::function<void(const std::uint16_t, const std::uint16_t, const std::uint8_t)> callback)
193{
194 onInputStatus = std::move(callback);
195}
196
197void CyclicController::OnInput(std::function<void(int, int, const Bytes&)> callback)
198{
199 onInputData = std::move(callback);
200}
201void CyclicController::OnTimeout(std::function<void()> callback)
202{
203 onTimeout = std::move(callback);
204}
205void CyclicController::OnError(std::function<void(const std::string&)> callback)
206{
207 onError = std::move(callback);
208}
209void CyclicController::OnStateChange(std::function<void(CyclicState, CyclicState)> callback)
210{
211 onStateChange = std::move(callback);
212}
213
215{
216 const CyclicState s = state;
218 {
219 throw std::runtime_error("Cannot start from " + ToString(s) + " state");
220 }
221
223 running = true;
224 stats.Reset();
225 lastRxCycleCounter.reset();
226
227 txSock.emplace(interface, etherType);
228 // The RX socket binds ETH_P_ALL:
229 // a socket bound to 0x8892 never sees VLAN-tagged frames unless the
230 // NIC strips the tag, and devices commonly send priority-tagged RT.
231 rxSock.emplace(interface, 0); // ETH_P_ALL
232 rxSock->SetTimeout(std::chrono::milliseconds(1));
233
235
236 txThread = std::thread([this]
237 { TxLoop(); });
238 rxThread = std::thread([this]
239 { RxLoop(); });
240
242}
243
245{
246 if (!running)
247 {
248 return;
249 }
250
252
253 // 1. Signal threads to exit before sending stop frames.
254 running = false;
255
256 // 2. Wait for TX thread first -- no concurrent socket access during stop frames.
257 if (txThread.joinable())
258 {
259 txThread.join();
260 }
261
262 // 3. Send STOP frames after TX thread has exited.
264
265 // 4. Close RX socket to unblock recv, then wait for RX thread.
266 rxSock.reset();
267 if (rxThread.joinable())
268 {
269 rxThread.join();
270 }
271
272 // 5. Close TX socket after stop frames sent.
273 txSock.reset();
274
276}
277
279{
280 CyclicStatsSnapshot snapshot;
281
282 snapshot.framesSent = stats.framesSent.load(std::memory_order_relaxed);
283
284 snapshot.framesReceived = stats.framesReceived.load(std::memory_order_relaxed);
285
286 snapshot.framesMissed = stats.framesMissed.load(std::memory_order_relaxed);
287
288 snapshot.framesInvalid = stats.framesInvalid.load(std::memory_order_relaxed);
289
290 snapshot.framesDuplicate = stats.framesDuplicate.load(std::memory_order_relaxed);
291
292 snapshot.framesOutOfOrder = stats.framesOutOfOrder.load(std::memory_order_relaxed);
293
294 snapshot.lastCycleTimeUs = stats.lastCycleTimeUs.load(std::memory_order_relaxed);
295
296 snapshot.maxJitterUs = stats.maxJitterUs.load(std::memory_order_relaxed);
297
298 snapshot.minCycleTimeUs = stats.minCycleTimeUs.load(std::memory_order_relaxed);
299
300 snapshot.maxCycleTimeUs = stats.maxCycleTimeUs.load(std::memory_order_relaxed);
301
302 snapshot.lastReceiveTime = stats.lastReceiveTime.load(std::memory_order_relaxed);
303
304 snapshot.consecutiveTimeouts = stats.consecutiveTimeouts.load(std::memory_order_relaxed);
305
306 snapshot.cycleTimeSumUs = stats.cycleTimeSumUs.load(std::memory_order_relaxed);
307
308 snapshot.cycleCount = stats.cycleCount.load(std::memory_order_relaxed);
309
310 return snapshot;
311}
312
314{
316
318
319 stats.framesSent.fetch_add(1, std::memory_order_relaxed);
320}
321
323{
324 const double cycleTimeS = outputIocr.CycleTimeUs() / 1'000'000.0;
325 auto nextSend = std::chrono::steady_clock::now();
326 auto lastSend = nextSend;
327 bool firstFrame = true;
328
329 while (running)
330 {
331 auto now = std::chrono::steady_clock::now();
332
333 if (now >= nextSend)
334 {
335 /*
336 if (state != CyclicState::Fault)
337 {
338 outputBuilder.Swap();
339 SendOutputFrame();
340 stats.framesSent.fetch_add(1, std::memory_order_relaxed);
341 }
342 */
343 TxCycle();
344
345 if (firstFrame)
346 {
347 firstFrame = false;
348 }
349 else
350 {
351 auto actualUs =
352 static_cast<std::uint32_t>(std::chrono::duration_cast<std::chrono::microseconds>(now - lastSend)
353 .count());
354 stats.lastCycleTimeUs = actualUs;
355 const std::uint32_t target = outputIocr.CycleTimeUs();
356 const std::uint32_t jitter = actualUs > target ? actualUs - target : target - actualUs;
357 const std::uint32_t prevMax = stats.maxJitterUs.load();
358 if (jitter > prevMax)
359 {
360 stats.maxJitterUs = jitter;
361 }
362 const std::uint32_t prevMin = stats.minCycleTimeUs.load();
363 if (actualUs < prevMin)
364 {
365 stats.minCycleTimeUs = actualUs;
366 }
367 const std::uint32_t prevMaxCycle = stats.maxCycleTimeUs.load();
368 if (actualUs > prevMaxCycle)
369 {
370 stats.maxCycleTimeUs = actualUs;
371 }
372 stats.cycleTimeSumUs += actualUs;
373 stats.cycleCount += 1;
374 }
375
376 lastSend = now;
377
378 auto cycleDuration = std::chrono::duration_cast<std::chrono::steady_clock::duration>(
379 std::chrono::duration<double>(cycleTimeS));
380 nextSend += cycleDuration;
381
382 if (nextSend < now)
383 {
384 nextSend = now + cycleDuration;
385 }
386 }
387
388 auto sleepTime = nextSend - std::chrono::steady_clock::now() - std::chrono::microseconds(100);
389 if (sleepTime > std::chrono::steady_clock::duration::zero())
390 {
391 std::this_thread::sleep_for(sleepTime);
392 }
393 }
394}
395
396void CyclicController::SendOutputFrame(std::optional<std::uint8_t> dataStatusOverride)
397{
398 cycleCounter = static_cast<std::uint16_t>((cycleCounter + txCounterStep) & 0xFFFF);
399
401
402 const std::uint8_t dataStatus = dataStatusOverride.value_or(
405 // const std::uint8_t dataStatus = 0xc0;
406
407 rt::RTFrame frame;
408 frame.frameId = outputIocr.frameId;
410 frame.dataStatus = dataStatus;
411 // frame.dataStatus = 0x80; // Set the PROFINET DataState byte to GOOD (0x80)
412 frame.transferStatus = 0x00;
413 // frame.transferStatus = 0x80; // Set the TranferStatus byte to GOOD (0x80)
414 frame.payload = payload;
415
416 const Bytes ethFrame = rt::BuildEthernetFrame(dstMac, srcMac, frame);
417
418 try
419 {
420 if (txSock)
421 {
422 txSock->Send(ethFrame);
423 }
424 }
425 catch (const std::exception& e)
426 {
427 if (onError)
428 {
429 onError(std::string("TX error: ") + e.what());
430 }
431 }
432}
433
435{
436 if (!txSock)
437 {
438 return;
439 }
440
441 auto stopStatus = static_cast<std::uint8_t>(rt::DATA_STATUS_VALID | rt::DATA_STATUS_STATION_OK |
443 const double cycleTimeS = outputIocr.CycleTimeUs() / 1'000'000.0;
444
445 for (int i = 0; i < STOP_FRAME_COUNT; ++i)
446 {
447 try
448 {
450 SendOutputFrame(stopStatus);
451 stats.framesSent.fetch_add(1, std::memory_order_relaxed);
452 }
453 catch (const std::exception&)
454 {
455 break;
456 }
457 if (i < STOP_FRAME_COUNT - 1)
458 {
459 std::this_thread::sleep_for(std::chrono::duration<double>(cycleTimeS));
460 }
461 }
462}
463
465{
466 const double watchdogSeconds = inputIocr.WatchdogTimeUs() / 1'000'000.0;
467 stats.lastReceiveTime = std::chrono::steady_clock::now();
468
469 while (running)
470 {
471 if (!rxSock)
472 {
473 break;
474 }
475 Bytes data;
476 try
477 {
478 data = rxSock->Recv();
479 }
480 catch (const std::exception& e)
481 {
482 if (running && onError)
483 {
484 onError(std::string("RX error: ") + e.what());
485 }
486 continue;
487 }
488
489 if (data.empty())
490 {
491 // Timeout (or socket closed returning 0 bytes) - check watchdog.
492 auto elapsed = std::chrono::duration<double>(std::chrono::steady_clock::now() - stats.lastReceiveTime.load(std::memory_order::relaxed)).count();
493 if (elapsed > watchdogSeconds)
494 {
496 stats.lastReceiveTime = std::chrono::steady_clock::now();
497 }
498 continue;
499 }
500
501 ProcessInputFrame(data);
502 }
503}
504
506{
507 stats.framesMissed.fetch_add(1, std::memory_order_relaxed);
508 const int consecutive = stats.consecutiveTimeouts.fetch_add(1, std::memory_order_relaxed) + 1;
509
511 {
512 // Set IOCS to BAD only when watchdog faulting is enabled. With
513 // max_consecutive_timeouts=0 the watchdog is monitoring-only; keep
514 // consumer status GOOD so transient RX gaps do not make the device drop
515 // an otherwise active output relationship.
517 iocsGood = false;
518 }
519
520 if (onTimeout)
521 {
522 try
523 {
524 onTimeout();
525 }
526 // NOLINTNEXTLINE(bugprone-empty-catch)
527 catch (...)
528 {
529 // proceed
530 }
531 }
532
534 {
536 if (onError)
537 {
538 onError("Communication lost: " + std::to_string(consecutive) + " consecutive watchdog timeouts");
539 }
540 }
541}
542
544{
545 // std::cout << "RX raw frame: "
546 // << data.size()
547 // << " bytes\n";
548
549 if (data.size() < 18)
550 {
551 // std::cout << " DROP: frame too short\n";
552 return;
553 }
554 // Parse Ethernet header; devices may send 802.1Q priority-tagged frames
555 MacAddress srcMacData{};
556 std::copy(data.begin() + 6, data.begin() + 12, srcMacData.begin());
557 size_t ethOffset = SkipVlanTags(data);
558 if (ethOffset + 2 > data.size())
559 {
560 // std::cout << " DROP: invalid Ethernet offset\n";
561 return;
562 }
563 const auto ethertype = static_cast<std::uint16_t>((data[ethOffset] << OneOctetShift) | data[ethOffset + 1]);
564 const bool fromDevice = srcMacData == dstMac;
565
566 // std::cout
567 // << "Ethernet frame: "
568 // << data.size()
569 // << " bytes"
570 // << " src="
571 // << profinet::Mac2String(srcMacData)
572 // << " dst="
573 // << profinet::Mac2String(dstMac)
574 // << (fromDevice ? " [AUMA]" : " [local]")
575 // << '\n';
576
577 if (ethertype != rt::ETHERTYPE_PROFINET)
578 {
579 // std::cout << " DROP: not PROFINET RT\n";
580 return;
581 }
582
583 if (srcMacData != dstMac)
584 {
585 // std::cout << " DROP: unexpected source MAC, expected "
586 // << Mac2String(dstMac)
587 // << '\n';
588 return;
589 }
590
591 std::optional<rt::RTFrame> frameOpt;
592 try
593 {
594 // frameOpt = rt::RTFrame::FromBytes(Bytes(data.begin() + ethOffset + 3, data.end())); // ethOffset + 3 kan ook 14 of 13 zijn..
595 frameOpt = rt::RTFrame::FromBytes(Bytes(data.begin() + ethOffset + 2, data.end())); // ethOffset + 3 kan ook 14 of 13 zijn..
596 }
597 catch (const std::exception&)
598 {
599 return;
600 }
601 rt::RTFrame& frame = *frameOpt;
602 // std::cout
603 // << " RT FrameID=0x"
604 // << std::uppercase
605 // << std::hex
606 // << frame.frameId
607 // << " expected=0x"
608 // << inputIocr.frameId
609 // << " cycleCounter=0x"
610 // << frame.cycleCounter
611 // << std::dec
612 // << '\n';
613 if (frame.frameId != inputIocr.frameId)
614 {
615 // std::cout << " DROP: FrameID mismatch\n";
616 return;
617 }
618
619 stats.lastReceiveTime = std::chrono::steady_clock::now();
620 stats.framesReceived.fetch_add(1, std::memory_order_relaxed);
622
624 {
626 }
627
629 // Check validity. TransferStatus != 0 means the provider flagged a
630 // transfer problem; conformant consumers discard such frames.
631 if (!frame.IsValid() || frame.transferStatus != 0)
632 {
633 stats.framesInvalid.fetch_add(1, std::memory_order_relaxed);
634 return;
635 }
636
637 // Set IOCS to GOOD - we received valid input data. Only rewrite the
638 // buffer on a BAD->GOOD transition; set_all_iocs dirties the whole
639 // send buffer and this runs for every received frame.
640 if (!iocsGood)
641 {
643 iocsGood = true;
644 }
645
646 std::lock_guard<std::mutex> lock(inputLock);
647 // Extract data per IO object. Each submodule's payload is followed by
648 // its IOPS byte; the device sets it BAD to disown the data it is still
649 // sending, so the payload is only usable while IOPS reports GOOD.
650 std::map<std::pair<std::uint16_t, std::uint16_t>, std::uint8_t> statusEvents;
651 std::map<std::pair<std::uint16_t, std::uint16_t>, Bytes> dataEvents;
652
653 for (const auto& obj : inputIocr.objects)
654 {
655 if (static_cast<std::size_t>(obj.offsetIOPS) > frame.payload.size())
656 {
657 continue;
658 }
659 if (static_cast<std::size_t>(obj.frameOffset + obj.dataLength) > frame.payload.size())
660 {
661 continue;
662 }
663
664 std::uint8_t iops = frame.payload[obj.offsetIOPS];
665 auto key = std::make_pair(obj.slot, obj.subslot);
666 bool wasGood = LocalIsInputGood(obj.slot, obj.subslot);
667 bool isGood = (iops & rt::IOXS_DATA_STATE_GOOD) != 0;
668 inputStatus[key] = iops;
669 const Bytes objData(frame.payload.begin() + obj.frameOffset,
670 frame.payload.begin() + obj.frameOffset + obj.dataLength);
671 // inputData[{obj.slot, obj.subslot}] = objData;
672 inputData[key] = objData;
673
674 if (isGood != wasGood)
675 {
676 statusEvents[key] = iops;
677 }
678 if (isGood)
679 {
680 dataEvents[key] = objData;
681 }
682 }
683
684 // Callbacks run outside the lock: they are application code and must not
685 // be able to stall the RX thread's next frame while holding it.
686 for (const auto& [key, iops] : statusEvents)
687 if (auto [slot, subslot] = key; true)
688 {
689 if (!(iops & rt::IOXS_DATA_STATE_GOOD))
690 {
691 /*
692 logger.warning(
693 f"Slot {slot}/{subslot}: device reports IOPS BAD (0x{iops:02X}), "
694 f"input data is no longer valid"
695 )
696 */
697 }
698 if (onInputStatus)
699 {
700 try
701 {
702 onInputStatus(slot, subslot, iops);
703 }
704 // NOLINTNEXTLINE(bugprone-empty-catch)
705 catch (...)
706 {
707 // continue to next -> logger.error(f"Input status callback error: {e}")
708 }
709 }
710 }
711 if (onInputData)
712 {
713 for (const auto& [key, objData] : dataEvents)
714 if (auto [slot, subslot] = key; true)
715 {
716 if (!objData.empty())
717 {
718 try
719 {
720 onInputData(slot, subslot, objData);
721 }
722 // NOLINTNEXTLINE(bugprone-empty-catch)
723 catch (...)
724 {
725 // continue to next //-->logger.error(f "Input callback error: {e}")
726 }
727 }
728 }
729 }
730}
731
732void CyclicController::TrackCycleCounter(std::uint16_t rxCounter)
733{
735 {
736 lastRxCycleCounter = rxCounter;
737 return;
738 }
739
740 const std::uint32_t step = rxCounterStep;
741 const auto expected = static_cast<std::uint16_t>((*lastRxCycleCounter + step) & 0xFFFF);
742
743 if (rxCounter == *lastRxCycleCounter)
744 {
745 stats.framesDuplicate.fetch_add(1, std::memory_order_relaxed);
746 }
747 else if (rxCounter != expected)
748 {
749 const auto forward = static_cast<std::uint16_t>((rxCounter - *lastRxCycleCounter) & 0xFFFF);
750 if (forward > 0x8000)
751 {
752 stats.framesOutOfOrder.fetch_add(1, std::memory_order_relaxed);
753 }
754 else if (step > 0)
755 {
756 const std::uint32_t gap = (forward / step) - 1;
757 if (gap > 0)
758 {
759 stats.framesMissed.fetch_add(gap, std::memory_order_relaxed);
760 }
761 }
762 lastRxCycleCounter = rxCounter;
763 }
764 else
765 {
766 lastRxCycleCounter = rxCounter;
767 }
768}
769
770} // namespace profinet::cyclic
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:192
bool iocsGood
Tracks the watchdog-driven IOCS state so the RX/timeout paths only rewrite the buffer on transitions ...
Definition cyclic.h:504
void TrackCycleCounter(std::uint16_t rxCounter)
Update cycle-counter tracking stats (gaps/duplicates/reordering) for a received frame.
Definition cyclic.cpp:732
std::uint32_t rxCounterStep
Expected cycle-counter increment per received input frame.
Definition cyclic.h:479
rt::CyclicDataBuilder outputBuilder
Double-buffered builder for output cyclic data.
Definition cyclic.h:467
void SetOutputData(int slot, int subslot, const Bytes &data)
Set output data for the next cycle.
Definition cyclic.cpp:137
std::uint32_t txCounterStep
Cycle-counter increment applied per sent output frame.
Definition cyclic.h:482
std::string interface
Network interface name.
Definition cyclic.h:425
void SendOutputFrame(std::optional< std::uint8_t > dataStatusOverride=std::nullopt)
Build and send one output frame.
Definition cyclic.cpp:396
void TxLoop()
TX thread body: sends output frames at the configured cycle rate.
Definition cyclic.cpp:322
std::mutex inputLock
Guards inputData against concurrent access.
Definition cyclic.h:470
void TxCycle()
Swap the output buffer and transmit one cyclic output frame.
Definition cyclic.cpp:313
std::thread txThread
Thread sending output frames.
Definition cyclic.h:452
void SendStopFrames()
Send STOP_FRAME_COUNT output frames with ProviderRun cleared.
Definition cyclic.cpp:434
rt::IOCRConfig outputIocr
Configuration for the output (controller -> device) IOCR.
Definition cyclic.h:437
void OnError(std::function< void(const std::string &)> callback)
Register a callback invoked on TX/RX socket errors.
Definition cyclic.cpp:205
std::function< void(int, int, const std::uint8_t)> onInputStatus
Registered OnInputStatus callback, if any.
Definition cyclic.h:485
void OnStateChange(std::function< void(CyclicState oldState, CyclicState newState)> callback)
Register a callback invoked on every state transition.
Definition cyclic.cpp:209
void Stop() override
Stop gracefully.
Definition cyclic.cpp:244
~CyclicController() override
Stop (if running) and release resources.
Definition cyclic.cpp:92
std::atomic< bool > running
Whether the TX/RX threads should keep running.
Definition cyclic.h:449
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:100
void RxLoop()
RX thread body: receives and processes input frames, checking the watchdog.
Definition cyclic.cpp:464
std::optional< Bytes > GetInputData(int slot, int subslot, bool allowBad=false) const
Get the latest input data received from the device, if any or IOPS is BAD.
Definition cyclic.cpp:148
void HandleWatchdogTimeout()
Handle a watchdog timeout: update stats and possibly transition to Fault.
Definition cyclic.cpp:505
CyclicController(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:60
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:418
std::function< void()> onTimeout
Registered OnTimeout callback, if any.
Definition cyclic.h:491
int maxConsecutiveTimeouts
Consecutive watchdog timeouts before transitioning to Fault.
Definition cyclic.h:440
rt::IOCRConfig inputIocr
Configuration for the input (device -> controller) IOCR.
Definition cyclic.h:434
std::optional< EthernetSocket > rxSock
Raw socket used for receiving input frames.
Definition cyclic.h:461
MacAddress srcMac
This host's MAC address.
Definition cyclic.h:428
void OnTimeout(std::function< void()> callback)
Register a callback invoked on each watchdog timeout.
Definition cyclic.cpp:201
bool LocalIsInputGood(std::uint16_t slot, std::uint16_t subslot) const
Check whether a submodule has GOOD provider status without locking.
Definition cyclic.cpp:180
CyclicStatsSnapshot StatsSnapshot() const
Definition cyclic.cpp:278
std::atomic< CyclicState > state
Current lifecycle state.
Definition cyclic.h:443
bool IsInputGood(std::uint16_t slot, std::uint16_t subslot) const
Check whether the device currently reports GOOD provider status.
Definition cyclic.cpp:173
std::function< void(CyclicState, CyclicState)> onStateChange
Registered OnStateChange callback, if any.
Definition cyclic.h:497
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:159
std::function< void(const std::string &)> onError
Registered OnError callback, if any.
Definition cyclic.h:494
std::uint16_t etherType
Current lifecycle state.
Definition cyclic.h:446
std::optional< EthernetSocket > txSock
Raw socket used for sending output frames.
Definition cyclic.h:458
std::map< std::pair< int, int >, Bytes > inputData
Latest received data per (slot, subslot).
Definition cyclic.h:473
std::thread rxThread
Thread receiving input frames.
Definition cyclic.h:455
MacAddress dstMac
The device's MAC address.
Definition cyclic.h:431
std::function< void(int, int, const Bytes &)> onInputData
Registered OnInput callback, if any.
Definition cyclic.h:488
void ProcessInputFrame(const Bytes &data)
Parse and process one received input frame.
Definition cyclic.cpp:543
void Start() override
Create TX/RX sockets and spawn the TX/RX threads.
Definition cyclic.cpp:214
std::uint16_t cycleCounter
Current TX cycle counter value.
Definition cyclic.h:464
CyclicStats stats
Communication statistics.
Definition cyclic.h:500
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:197
std::optional< std::uint16_t > lastRxCycleCounter
Last received cycle counter, for gap/duplicate/reorder detection.
Definition cyclic.h:476
void SetAllIocs(std::uint8_t status=IOXS_GOOD)
Set every object's IOCS byte to the same value.
Definition rt.cpp:169
void SetData(int slot, int subslot, const Bytes &data)
Write one object's data into the write buffer.
Definition rt.cpp:89
void SetIops(int slot, int subslot, std::uint8_t status=IOXS_GOOD)
Set one object's IOPS (provider status) byte.
Definition rt.cpp:122
void Swap()
Promote the write buffer to the send buffer.
Definition rt.cpp:189
Bytes Build() const
Get the current send buffer contents.
Definition rt.cpp:199
void SetAllIops(std::uint8_t status=IOXS_GOOD)
Set every object's IOPS byte to the same value.
Definition rt.cpp:156
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:79
std::string ToString(CyclicState state)
Human-readable name for a CyclicState value.
Definition cyclic.cpp:22
CyclicState
Lifecycle state of a CyclicController.
Definition cyclic.h:39
@ 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:157
constexpr std::uint8_t IOXS_BAD
IOxS value: bad.
Definition rt.h:95
constexpr std::uint8_t DATA_STATUS_STATION_OK
DataStatus bit: station health, 0=Problem, 1=OK.
Definition rt.h:83
constexpr std::uint8_t DATA_STATUS_PROVIDER_RUN
DataStatus bit: provider run state, 0=Stop, 1=Run.
Definition rt.h:81
Bytes BuildEthernetFrame(const MacAddress &dstMac, const MacAddress &srcMac, const RTFrame &rtFrame)
Build a complete Ethernet frame carrying an RT frame.
Definition rt.cpp:212
constexpr std::uint8_t DATA_STATUS_STATE
DataStatus bit: provider state, 0=Backup, 1=Primary.
Definition rt.h:73
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:77
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:93
constexpr std::uint8_t IOXS_GOOD
IOxS value: good, subslot level.
Definition rt.h:89
std::array< std::uint8_t, macAddressLength > MacAddress
A 6-byte Ethernet MAC address.
Definition util.h:67
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:89
static constexpr int OneOctetShift
The bit-shift distance required to move data across a single octet.
Definition util.h:50
std::vector< std::uint8_t > Bytes
Generic byte buffer alias used throughout the library for raw wire data.
Definition protocol.h:52
Bytes payload
Definition rpc.cpp:3254
Copyable snapshot of cyclic communication statistics.
Definition cyclic.h:121
std::chrono::steady_clock::time_point lastReceiveTime
Definition cyclic.h:134
void Reset()
Reset all counters to their initial values.
Definition cyclic.cpp:42
std::atomic< std::uint32_t > maxJitterUs
Largest observed deviation from the target cycle time, in microseconds.
Definition cyclic.h:88
std::atomic< std::uint64_t > framesReceived
Total number of input frames received.
Definition cyclic.h:70
std::atomic< std::uint64_t > framesInvalid
Total number of frames rejected for having an invalid DataStatus.
Definition cyclic.h:76
std::atomic< std::uint64_t > framesOutOfOrder
Total number of frames received out of cycle-counter order.
Definition cyclic.h:82
std::atomic< std::uint32_t > minCycleTimeUs
Smallest observed TX cycle duration, in microseconds.
Definition cyclic.h:91
std::atomic< std::uint64_t > cycleTimeSumUs
Running sum of observed TX cycle durations, in microseconds.
Definition cyclic.h:103
std::atomic< std::uint64_t > framesDuplicate
Total number of duplicate (repeated cycle counter) frames received.
Definition cyclic.h:79
std::atomic< std::uint64_t > framesMissed
Total number of watchdog timeouts (missed input frames).
Definition cyclic.h:73
std::atomic< std::uint32_t > lastCycleTimeUs
Duration of the most recently completed TX cycle, in microseconds.
Definition cyclic.h:85
std::atomic< std::uint64_t > framesSent
Total number of output frames sent.
Definition cyclic.h:67
std::atomic< std::uint64_t > cycleCount
Number of TX cycles included in CycleTimeSumUs.
Definition cyclic.h:106
std::atomic< int > consecutiveTimeouts
Current run of consecutive watchdog timeouts (resets on a good frame).
Definition cyclic.h:100
std::atomic< std::chrono::steady_clock::time_point > lastReceiveTime
Timestamp of the last received input frame (or watchdog check).
Definition cyclic.h:97
std::atomic< std::uint32_t > maxCycleTimeUs
Largest observed TX cycle duration, in microseconds.
Definition cyclic.h:94
IOCR configuration derived from AR setup: timing parameters and IO object mappings needed for cyclic ...
Definition rt.h:126
std::uint32_t CycleTimeUs() const
Compute the cycle time in microseconds.
Definition rt.h:160
std::vector< IODataObject > objects
IO data objects carried in this IOCR's cyclic frame.
Definition rt.h:152
std::uint16_t sendClockFactor
Send clock base factor (31.25us units).
Definition rt.h:137
double CycleTimeMs() const
Compute the cycle time in milliseconds.
Definition rt.h:167
std::uint16_t frameId
Frame ID assigned to this IOCR.
Definition rt.h:134
std::uint16_t reductionRatio
Reduction ratio relative to the send clock.
Definition rt.h:140
std::uint32_t WatchdogTimeUs() const
Compute the watchdog timeout in microseconds.
Definition rt.h:174
A single PROFINET Real-Time cyclic frame: Frame ID + C_SDU payload + cycle counter/status trailer.
Definition rt.h:197
std::uint8_t dataStatus
Data status bitmask (see DATA_STATUS_* constants).
Definition rt.h:205
std::uint8_t transferStatus
Transfer status (0 = OK).
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:199
std::uint16_t cycleCounter
Cycle counter, incremented each transmission.
Definition rt.h:202
Bytes payload
C_SDU payload: process data plus IOPS/IOCS trailers.
Definition rt.h:211
bool IsValid() const
Whether the DataStatus valid bit is set.
Definition rt.h:239