64 int maxConsecutiveTimeouts, std::uint16_t etherType)
66 interface(std::move(interface)),
69 inputIocr(std::move(inputIocr)),
70 outputIocr(std::move(outputIocr)),
71 maxConsecutiveTimeouts(maxConsecutiveTimeouts),
73 outputBuilder(this->outputIocr),
76 if (this->maxConsecutiveTimeouts < 0)
78 throw std::invalid_argument(
"max_consecutive_timeouts must be >= 0, got " +
79 std::to_string(this->maxConsecutiveTimeouts));
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.",
114 throw std::invalid_argument(message);
142 throw std::runtime_error(
"Cannot set output data in " +
ToString(s) +
" state");
154 const std::lock_guard<std::mutex> lock(
inputLock);
155 auto it =
inputData.find({slot, subslot});
165 const std::lock_guard<std::mutex> lock(
inputLock);
179 const std::lock_guard<std::mutex> lock(
inputLock);
239 throw std::runtime_error(
"Cannot start from " +
ToString(s) +
" state");
260 std::jthread txWorker;
261 std::jthread rxWorker;
264 rx->SetTimeout(std::chrono::milliseconds(1));
272 txWorker = std::jthread([
this]
274 rxWorker = std::jthread([
this]
281 throw std::runtime_error(
"Cannot start cyclic controller.");
373 auto nextSend = std::chrono::steady_clock::now();
374 auto lastSend = nextSend;
375 bool firstFrame =
true;
379 auto now = std::chrono::steady_clock::now();
400 static_cast<std::uint32_t
>(std::chrono::duration_cast<std::chrono::microseconds>(now - lastSend)
404 const std::uint32_t jitter = actualUs > target ? actualUs - target : target - actualUs;
406 if (jitter > prevMax)
411 if (actualUs < prevMin)
416 if (actualUs > prevMaxCycle)
426 auto cycleDuration = std::chrono::duration_cast<std::chrono::steady_clock::duration>(
427 std::chrono::duration<double>(cycleTimeS));
428 nextSend += cycleDuration;
432 nextSend = now + cycleDuration;
436 auto sleepTime = nextSend - std::chrono::steady_clock::now() - std::chrono::microseconds(100);
437 if (sleepTime > std::chrono::steady_clock::duration::zero())
439 std::this_thread::sleep_for(sleepTime);
450 const std::uint8_t dataStatus = dataStatusOverride.value_or(
458 frame.dataStatus = dataStatus;
460 frame.transferStatus = 0x00;
477 catch (
const std::exception& e)
481 onError(std::string(
"TX error: ") + e.what());
505 catch (
const std::exception&)
511 std::this_thread::sleep_for(std::chrono::duration<double>(cycleTimeS));
532 catch (
const std::exception& e)
536 onError(std::string(
"RX error: ") + e.what());
544 auto elapsed = std::chrono::duration<double>(std::chrono::steady_clock::now() -
stats.
lastReceiveTime.load(std::memory_order::relaxed)).count();
545 if (elapsed > watchdogSeconds)
590 onError(
"Communication lost: " + std::to_string(consecutive) +
" consecutive watchdog timeouts");
598 if (data.size() < (2 *
macAddressLength) + (2 *
sizeof(std::uint16_t)) +
sizeof(std::uint16_t))
611 if (data.size() < ethOffset +
sizeof(std::uint16_t))
618 const auto ethertype = r.
U16();
619 ethOffset +=
sizeof(std::uint16_t);
647 std::optional<rt::RTFrame> frameOpt;
653 catch (
const std::exception&)
687 if (!
frame.IsValid() ||
frame.transferStatus != 0)
702 std::lock_guard<std::mutex> lock(
inputLock);
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;
711 if (!
obj.offsetIOPS.has_value())
715 if (
static_cast<std::size_t
>(*
obj.offsetIOPS) >
frame.payload.size())
719 if (
static_cast<std::size_t
>(
obj.frameOffset +
obj.dataLength) >
frame.payload.size())
724 std::uint8_t iops =
frame.payload[*
obj.offsetIOPS];
725 auto key = std::make_pair(
obj.slot,
obj.subslot);
730 frame.payload.begin() +
obj.frameOffset +
obj.dataLength);
734 if (isGood != wasGood)
736 statusEvents[key] = iops;
740 dataEvents[key] = objData;
746 for (
const auto& [key, iops] : statusEvents)
747 if (
auto [slot, subslot] = key;
true)
759 onError(std::format(
"Slot {}/{}: device reports IOPS BAD ({}), input data is no longer valid", slot, subslot,
Hex2(iops)));
777 for (
const auto& [key, objData] : dataEvents)
778 if (
auto [slot, subslot] = key;
true)
780 if (!objData.empty())
805 const auto expected =
static_cast<std::uint16_t
>((*
lastRxCycleCounter + step) & 0xFFFF);
811 else if (rxCounter != expected)
813 const auto forward =
static_cast<std::uint16_t
>((rxCounter - *
lastRxCycleCounter) & 0xFFFF);
814 if (forward > 0x8000)
820 const std::uint32_t gap = (forward / step) - 1;
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.
bool iocsGood
Tracks the watchdog-driven IOCS state so the RX/timeout paths only rewrite the buffer on transitions ...
void TrackCycleCounter(std::uint16_t rxCounter)
Update cycle-counter tracking stats (gaps/duplicates/reordering) for a received frame.
std::uint32_t rxCounterStep
Expected cycle-counter increment per received input frame.
rt::CyclicDataBuilder outputBuilder
Double-buffered builder for output cyclic data.
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.
std::uint32_t txCounterStep
Cycle-counter increment applied per sent output frame.
std::mutex callbackMutex
Guards callback setting against concurrent access.
std::string interface
Network interface name.
void SendOutputFrame(std::optional< std::uint8_t > dataStatusOverride=std::nullopt)
Build and send one output frame.
std::unique_ptr< IRawEthernetSocket > rxSock
Raw socket used for receiving input frames.
void TxLoop()
TX thread body: sends output frames at the configured cycle rate.
asio::io_context & ioContext
Asio execution context used by the raw Ethernet transports.
std::jthread txThread
Thread sending output frames.
std::mutex inputLock
Guards inputData against concurrent access.
void TxCycle()
Swap the output buffer and transmit one cyclic output frame.
void SendStopFrames()
Send STOP_FRAME_COUNT output frames with ProviderRun cleared.
rt::IOCRConfig outputIocr
Configuration for the output (controller -> device) IOCR.
void OnError(std::function< void(const std::string &)> callback)
Register a callback invoked on TX/RX socket errors.
std::function< void(int, int, const std::uint8_t)> onInputStatus
Registered OnInputStatus callback, if any.
void OnStateChange(std::function< void(CyclicState oldState, CyclicState newState)> callback)
Register a callback invoked on every state transition.
void Stop() override
Stop gracefully.
~CyclicController() override
Stop (if running) and release resources.
void OnOutput(std::function< void(int cycleCounter, const Bytes &data)> callback)
Register a callback invoked whenever new output data is send.
std::atomic< bool > running
Whether the TX/RX threads should keep running.
void Transition(CyclicState newState)
Change state and invoke the OnStateChange callback if registered.
void CheckCycleTime()
Validate the configured cycle time, throwing if it's below 1ms.
void RxLoop()
RX thread body: receives and processes input frames, checking the watchdog.
void HandleWatchdogTimeout()
Handle a watchdog timeout: update stats and possibly transition to Fault.
std::jthread rxThread
Thread receiving input frames.
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...
void SetOutputData(int slot, int subslot, const Bytes &data) override
Set output data for the next cycle.
std::function< void()> onTimeout
Registered OnTimeout callback, if any.
int maxConsecutiveTimeouts
Consecutive watchdog timeouts before transitioning to Fault.
rt::IOCRConfig inputIocr
Configuration for the input (device -> controller) IOCR.
MacAddress srcMac
This host's MAC address.
void OnTimeout(std::function< void()> callback)
Register a callback invoked on each watchdog timeout.
bool LocalIsInputGood(std::uint16_t slot, std::uint16_t subslot) const
Check whether a submodule has GOOD provider status without locking.
CyclicStatsSnapshot StatsSnapshot() const
std::atomic< CyclicState > state
Current lifecycle state.
bool IsInputGood(std::uint16_t slot, std::uint16_t subslot) const
Check whether the device currently reports GOOD provider status.
std::function< void(CyclicState, CyclicState)> onStateChange
Registered OnStateChange callback, if any.
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.
std::function< void(const std::string &)> onError
Registered OnError callback, if any.
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.
std::uint16_t etherType
Current lifecycle state.
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.
std::unique_ptr< IRawEthernetSocket > txSock
Raw socket used for sending output frames.
std::map< std::pair< int, int >, Bytes > inputData
Latest received data per (slot, subslot).
MacAddress dstMac
The device's MAC address.
std::function< void(int, const Bytes &)> onOutputData
Registered OnOutputData callback, if any.
std::function< void(int, int, const Bytes &)> onInputData
Registered OnInput callback, if any.
void ProcessInputFrame(const Bytes &data)
Parse and process one received input frame.
std::uint16_t cycleCounter
Current TX cycle counter value.
Bytes GetOutputData(int slot, int subslot) const override
Get the current output data for a slot/subslot.
CyclicStats stats
Communication statistics.
void OnInput(std::function< void(int slot, int subslot, const Bytes &data)> callback)
Register a callback invoked whenever new input data is received.
std::optional< std::uint16_t > lastRxCycleCounter
Last received cycle counter, for gap/duplicate/reorder detection.
Bytes GetData(int slot, int subslot) const
Read one object's data from the write buffer.
void SetAllIocs(std::uint8_t status=IOXS_GOOD)
Set every object's IOCS byte to the same value.
void SetData(int slot, int subslot, const Bytes &data)
Write one object's data into the write buffer.
void SetIops(int slot, int subslot, std::uint8_t status=IOXS_GOOD)
Set one object's IOPS (provider status) byte.
void Swap()
Promote the write buffer to the send buffer.
Bytes Build() const
Get the current send buffer contents.
void SetAllIops(std::uint8_t status=IOXS_GOOD)
Set every object's IOPS byte to the same value.
Lightweight cursor for parsing a big-endian byte buffer without copies.
void SkipTo(std::size_t offset)
Move the read position to an absolute byte offset.
std::uint16_t U16()
Read and consume a big-endian 16-bit value.
Declares the PROFINET RTC1 cyclic IO controller and process-data exchange API.
Declares exceptions and error types used by the PROFINET IO controller stack.
std::string ToString(CyclicState state)
Human-readable name for a CyclicState value.
CyclicState
Lifecycle state of a CyclicController.
@ 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().
constexpr std::uint8_t IOXS_BAD
IOxS value: bad.
constexpr std::uint8_t DATA_STATUS_STATION_OK
DataStatus bit: station health, 0=Problem, 1=OK.
constexpr std::uint8_t DATA_STATUS_PROVIDER_RUN
DataStatus bit: provider run state, 0=Stop, 1=Run.
Bytes BuildEthernetFrame(const MacAddress &dstMac, const MacAddress &srcMac, const RTFrame &rtFrame)
Build a complete Ethernet frame carrying an RT frame.
constexpr std::uint8_t DATA_STATUS_STATE
DataStatus bit: provider state, 0=Backup, 1=Primary.
constexpr std::uint16_t ETHERTYPE_PROFINET
EtherType used by PROFINET RT frames (0x8892).
constexpr std::uint8_t DATA_STATUS_VALID
DataStatus bit: data validity, 0=Invalid, 1=Valid.
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,...
constexpr std::uint8_t IOXS_GOOD
IOxS value: good, subslot level.
std::string Hex2(std::uint8_t value)
Format an 8-bit unsigned integer as a 2-digit hexadecimal string.
std::array< std::uint8_t, macAddressLength > MacAddress
A 6-byte Ethernet MAC address.
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.
constexpr int macAddressLength
Constant lenght of a mac address.
std::vector< std::uint8_t > Bytes
Generic byte buffer alias used throughout the library for raw wire data.
Copyable snapshot of cyclic communication statistics.
std::uint64_t cycleTimeSumUs
std::uint32_t minCycleTimeUs
std::uint32_t maxJitterUs
std::uint64_t framesDuplicate
std::uint64_t framesOutOfOrder
std::uint64_t framesMissed
std::uint64_t framesReceived
std::chrono::steady_clock::time_point lastReceiveTime
std::uint64_t framesInvalid
std::uint32_t lastCycleTimeUs
std::uint32_t maxCycleTimeUs
void Reset()
Reset all counters to their initial values.
std::atomic< std::uint32_t > maxJitterUs
Largest observed deviation from the target cycle time, in microseconds.
std::atomic< std::uint64_t > framesReceived
Total number of input frames received.
std::atomic< std::uint64_t > framesInvalid
Total number of frames rejected for having an invalid DataStatus.
std::atomic< std::uint64_t > framesOutOfOrder
Total number of frames received out of cycle-counter order.
std::atomic< std::uint32_t > minCycleTimeUs
Smallest observed TX cycle duration, in microseconds.
std::atomic< std::uint64_t > cycleTimeSumUs
Running sum of observed TX cycle durations, in microseconds.
std::atomic< std::uint64_t > framesDuplicate
Total number of duplicate (repeated cycle counter) frames received.
std::atomic< std::uint64_t > framesMissed
Total number of watchdog timeouts (missed input frames).
std::atomic< std::uint32_t > lastCycleTimeUs
Duration of the most recently completed TX cycle, in microseconds.
std::atomic< std::uint64_t > framesSent
Total number of output frames sent.
std::atomic< std::uint64_t > cycleCount
Number of TX cycles included in CycleTimeSumUs.
std::atomic< int > consecutiveTimeouts
Current run of consecutive watchdog timeouts (resets on a good frame).
std::atomic< std::chrono::steady_clock::time_point > lastReceiveTime
Timestamp of the last received input frame (or watchdog check).
std::atomic< std::uint32_t > maxCycleTimeUs
Largest observed TX cycle duration, in microseconds.
IOCR configuration derived from AR setup: timing parameters and IO object mappings needed for cyclic ...
std::uint32_t CycleTimeUs() const
Compute the cycle time in microseconds.
std::vector< IODataObject > objects
IO data objects carried in this IOCR's cyclic frame.
std::uint16_t sendClockFactor
Send clock base factor (31.25us units).
double CycleTimeMs() const
Compute the cycle time in milliseconds.
std::uint16_t frameId
Frame ID assigned to this IOCR.
std::uint16_t reductionRatio
Reduction ratio relative to the send clock.
std::uint32_t WatchdogTimeUs() const
Compute the watchdog timeout in microseconds.
A single PROFINET Real-Time cyclic frame: Frame ID + C_SDU payload + cycle counter/status trailer.
static RTFrame FromBytes(const Bytes &data)
Parse an RT frame from raw bytes (after the Ethernet header).
std::uint16_t frameId
Frame ID identifying which IOCR this frame belongs to.
std::unique_ptr< MockRawEthernetSocket > tx
std::unique_ptr< MockRawEthernetSocket > rx
std::unique_ptr< asio::io_context > ioContext
Shared ASIO context.