62 int maxConsecutiveTimeouts, std::uint16_t etherType)
63 : interface(std::move(interface)),
66 inputIocr(std::move(inputIocr)),
67 outputIocr(std::move(outputIocr)),
68 maxConsecutiveTimeouts(maxConsecutiveTimeouts),
70 outputBuilder(this->outputIocr),
73 if (this->maxConsecutiveTimeouts < 0)
75 throw std::invalid_argument(
"max_consecutive_timeouts must be >= 0, got " +
76 std::to_string(this->maxConsecutiveTimeouts));
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.",
111 throw std::invalid_argument(message);
142 throw std::runtime_error(
"Cannot set output data in " +
ToString(s) +
" state");
150 const std::lock_guard<std::mutex> lock(
inputLock);
151 auto it =
inputData.find({slot, subslot});
161 const std::lock_guard<std::mutex> lock(
inputLock);
175 const std::lock_guard<std::mutex> lock(
inputLock);
219 throw std::runtime_error(
"Cannot start from " +
ToString(s) +
" state");
232 rxSock->SetTimeout(std::chrono::milliseconds(1));
325 auto nextSend = std::chrono::steady_clock::now();
326 auto lastSend = nextSend;
327 bool firstFrame =
true;
331 auto now = std::chrono::steady_clock::now();
352 static_cast<std::uint32_t
>(std::chrono::duration_cast<std::chrono::microseconds>(now - lastSend)
356 const std::uint32_t jitter = actualUs > target ? actualUs - target : target - actualUs;
358 if (jitter > prevMax)
363 if (actualUs < prevMin)
368 if (actualUs > prevMaxCycle)
378 auto cycleDuration = std::chrono::duration_cast<std::chrono::steady_clock::duration>(
379 std::chrono::duration<double>(cycleTimeS));
380 nextSend += cycleDuration;
384 nextSend = now + cycleDuration;
388 auto sleepTime = nextSend - std::chrono::steady_clock::now() - std::chrono::microseconds(100);
389 if (sleepTime > std::chrono::steady_clock::duration::zero())
391 std::this_thread::sleep_for(sleepTime);
402 const std::uint8_t dataStatus = dataStatusOverride.value_or(
425 catch (
const std::exception& e)
429 onError(std::string(
"TX error: ") + e.what());
453 catch (
const std::exception&)
459 std::this_thread::sleep_for(std::chrono::duration<double>(cycleTimeS));
480 catch (
const std::exception& e)
484 onError(std::string(
"RX error: ") + e.what());
492 auto elapsed = std::chrono::duration<double>(std::chrono::steady_clock::now() -
stats.
lastReceiveTime.load(std::memory_order::relaxed)).count();
493 if (elapsed > watchdogSeconds)
538 onError(
"Communication lost: " + std::to_string(consecutive) +
" consecutive watchdog timeouts");
549 if (data.size() < 18)
556 std::copy(data.begin() + 6, data.begin() + 12, srcMacData.begin());
558 if (ethOffset + 2 > data.size())
563 const auto ethertype =
static_cast<std::uint16_t
>((data[ethOffset] <<
OneOctetShift) | data[ethOffset + 1]);
564 const bool fromDevice = srcMacData ==
dstMac;
591 std::optional<rt::RTFrame> frameOpt;
597 catch (
const std::exception&)
646 std::lock_guard<std::mutex> lock(
inputLock);
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;
655 if (
static_cast<std::size_t
>(
obj.offsetIOPS) > frame.
payload.size())
659 if (
static_cast<std::size_t
>(
obj.frameOffset +
obj.dataLength) > frame.
payload.size())
664 std::uint8_t iops = frame.
payload[
obj.offsetIOPS];
665 auto key = std::make_pair(
obj.slot,
obj.subslot);
674 if (isGood != wasGood)
676 statusEvents[key] = iops;
680 dataEvents[key] = objData;
686 for (
const auto& [key, iops] : statusEvents)
687 if (
auto [slot, subslot] = key;
true)
713 for (
const auto& [key, objData] : dataEvents)
714 if (
auto [slot, subslot] = key;
true)
716 if (!objData.empty())
741 const auto expected =
static_cast<std::uint16_t
>((*
lastRxCycleCounter + step) & 0xFFFF);
747 else if (rxCounter != expected)
749 const auto forward =
static_cast<std::uint16_t
>((rxCounter - *
lastRxCycleCounter) & 0xFFFF);
750 if (forward > 0x8000)
756 const std::uint32_t gap = (forward / step) - 1;
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.
void SetOutputData(int slot, int subslot, const Bytes &data)
Set output data for the next cycle.
std::uint32_t txCounterStep
Cycle-counter increment applied per sent output frame.
std::string interface
Network interface name.
void SendOutputFrame(std::optional< std::uint8_t > dataStatusOverride=std::nullopt)
Build and send one output frame.
void TxLoop()
TX thread body: sends output frames at the configured cycle rate.
std::mutex inputLock
Guards inputData against concurrent access.
void TxCycle()
Swap the output buffer and transmit one cyclic output frame.
std::thread txThread
Thread sending output frames.
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.
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.
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.
void HandleWatchdogTimeout()
Handle a watchdog timeout: update stats and possibly transition to Fault.
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.
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...
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.
std::optional< EthernetSocket > rxSock
Raw socket used for receiving input frames.
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.
std::uint16_t etherType
Current lifecycle state.
std::optional< EthernetSocket > txSock
Raw socket used for sending output frames.
std::map< std::pair< int, int >, Bytes > inputData
Latest received data per (slot, subslot).
std::thread rxThread
Thread receiving input frames.
MacAddress dstMac
The device's MAC address.
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.
void Start() override
Create TX/RX sockets and spawn the TX/RX threads.
std::uint16_t cycleCounter
Current TX cycle counter value.
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.
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.
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::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.
static constexpr int OneOctetShift
The bit-shift distance required to move data across a single octet.
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.
std::uint8_t dataStatus
Data status bitmask (see DATA_STATUS_* constants).
std::uint8_t transferStatus
Transfer status (0 = OK).
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::uint16_t cycleCounter
Cycle counter, incremented each transmission.
Bytes payload
C_SDU payload: process data plus IOPS/IOCS trailers.
bool IsValid() const
Whether the DataStatus valid bit is set.