19using namespace std::chrono_literals;
21constexpr std::uint16_t kBlockHeaderSize = 4U;
22constexpr std::uint16_t kControlBlockSize = 32U;
24bool IsControlPrmBegin(
const std::uint16_t type)
29bool IsControlPrmEnd(
const std::uint16_t type)
34bool IsControlRelease(
const std::uint16_t type)
50 return "Parameterizing";
52 return "Parameterized";
54 return "ApplicationReady";
65 : config(std::move(configRef))
81 ioContext = std::make_unique<asio::io_context>();
84 asio::error_code
error;
89 throw std::system_error(
error,
"VirtualProfinetRpcDevice::open");
92 socket->set_option(asio::socket_base::reuse_address(
true),
error);
96 throw std::system_error(
error,
"VirtualProfinetRpcDevice::set_option");
100 asio::ip::udp::endpoint(asio::ip::udp::v4(),
config.
rpcPort),
105 throw std::system_error(
error,
"VirtualProfinetRpcDevice::bind");
108 worker = std::thread([
this]()
134 std::thread applicationReadyThread;
136 std::scoped_lock lock(
mutex);
140 if (applicationReadyThread.joinable())
142 applicationReadyThread.join();
145 asio::error_code
error;
165 asio::io_context wakeContext;
166 asio::ip::udp::socket wakeSocket(wakeContext);
168 asio::error_code
error;
170 wakeSocket.open(asio::ip::udp::v4(),
error);
176 constexpr std::uint8_t wakeByte = 0;
178 const asio::ip::udp::endpoint endpoint(
179 asio::ip::address_v4::loopback(),
183 asio::buffer(&wakeByte,
sizeof(wakeByte)),
197 std::scoped_lock lock(
mutex);
203 std::scoped_lock lock(
mutex);
209 std::scoped_lock lock(
mutex);
215 std::scoped_lock lock(
mutex);
221 std::scoped_lock lock(
mutex);
227 std::scoped_lock lock(
mutex);
233 std::scoped_lock lock(
mutex);
239 std::scoped_lock lock(
mutex);
245 std::scoped_lock lock(
mutex);
251 std::scoped_lock lock(
mutex);
257 std::scoped_lock lock(
mutex);
263 std::scoped_lock lock(
mutex);
269 const std::chrono::milliseconds timeout)
const
271 std::unique_lock lock(
mutex);
277 return state == expected;
291 asio::ip::udp::endpoint sender;
292 asio::error_code
error;
294 const std::size_t received =
socket->receive_from(
316 catch (
const std::exception& exception)
318 std::scoped_lock lock(
mutex);
328 const asio::ip::udp::endpoint& sender)
361 std::scoped_lock lock(
mutex);
375 std::unique_lock lock(
mutex);
382 return !running.load();
398 const asio::ip::udp::endpoint& sender)
410 std::scoped_lock lock(
mutex);
418 Bytes responsePayload;
423 std::size_t offset = 0;
424 while (offset + kBlockHeaderSize <= nrd.
payload.size())
427 const std::size_t blockSize =
428 kBlockHeaderSize +
static_cast<std::size_t
>(header.blockLength);
430 if (blockSize > nrd.
payload.size() - offset)
438 nrd.
payload.begin() +
static_cast<std::ptrdiff_t
>(offset),
439 nrd.
payload.begin() +
static_cast<std::ptrdiff_t
>(offset + blockSize));
444 responsePayload.insert(responsePayload.end(), response.begin(), response.end());
453 responsePayload.insert(responsePayload.end(), alarm.begin(), alarm.end());
461 const asio::ip::udp::endpoint& sender)
464 if (nrd.
payload.size() < kControlBlockSize)
470 const std::uint16_t blockType = reader.
U16();
472 const std::uint16_t command = reader.
U16();
476 std::scoped_lock lock(
mutex);
480 lastError =
"PrmBegin received outside Connected state";
493 std::scoped_lock lock(
mutex);
497 lastError =
"PrmEnd received before PrmBegin";
520 if (IsControlRelease(blockType))
523 std::scoped_lock lock(
mutex);
535 std::scoped_lock lock(
mutex);
540 lastError =
"ApplicationReady response received outside Parameterized state";
548 lastError =
"ApplicationReady response received without a request";
559 lastError =
"ApplicationReady response did not preserve RPC correlation";
574 std::scoped_lock lock(
mutex);
576 lastError =
"CONNECT missing ARBlockReq";
584 std::size_t offset = 0;
585 while (offset + kBlockHeaderSize <= nrdPayload.size())
588 const std::size_t blockSize =
589 kBlockHeaderSize +
static_cast<std::size_t
>(header.blockLength);
591 if (blockSize > nrdPayload.size() - offset)
599 nrdPayload.begin() +
static_cast<std::ptrdiff_t
>(offset),
600 nrdPayload.begin() +
static_cast<std::ptrdiff_t
>(offset + blockSize));
602 input = input || iocr.
iocrType == 1U;
603 output = output || iocr.
iocrType == 2U;
610 std::scoped_lock lock(
mutex);
621 const bool cyclicConnect = input || output;
624 std::scoped_lock lock(
mutex);
626 lastError =
"Cyclic CONNECT must contain both Input and Output IOCR";
633 std::scoped_lock lock(
mutex);
635 lastError =
"Cyclic CONNECT missing ExpectedSubmoduleBlockReq";
642 std::scoped_lock lock(
mutex);
644 lastError =
"Cyclic CONNECT missing AlarmCRBlockReq";
654 const Bytes& nrdPayload,
655 const asio::ip::udp::endpoint& sender)
660 asio::error_code
error;
661 socket->send_to(asio::buffer(bytes), sender, 0,
error);
664 std::scoped_lock lock(
mutex);
676 std::scoped_lock lock(
mutex);
709 request.
flags1 =
static_cast<std::uint8_t
>(
723 request.
lengthOfBody =
static_cast<std::uint16_t
>(nrd.size());
730 std::scoped_lock lock(
mutex);
736 asio::error_code
error;
741 std::scoped_lock lock(
mutex);
743 lastError =
"Failed to send ApplicationReady: " +
error.message();
762 const Bytes& nrdPayload)
766 response.
flags1 =
static_cast<std::uint8_t
>(
770 static_cast<std::uint16_t
>(nrdPayload.size());
791 const std::uint16_t deviceAlarmReference)
811 std::size_t offset = 0;
812 while (offset + kBlockHeaderSize <= data.size())
815 const std::size_t blockSize =
816 kBlockHeaderSize +
static_cast<std::size_t
>(header.blockLength);
818 if (blockSize > data.size() - offset)
823 if (header.blockType == blockType)
834 const Bytes& connectPayload,
835 std::array<std::uint8_t, uuidLenght>& arUuidOut,
836 std::uint16_t& sessionKeyOut)
838 if (connectPayload.size() < 8U +
uuidLenght +
sizeof(std::uint16_t))
840 throw std::invalid_argument(
"CONNECT ARBlockReq is truncated");
846 sessionKeyOut = reader.
U16();
static bool ContainsBlock(const Bytes &data, BlockType blockType)
Find a block type in a serialized CONNECT payload.
void WakeWorker() noexcept
Wake the synchronous receive so the worker can observe shutdown.
std::size_t PrmEndCount() const noexcept
Return the number of PrmEnd requests accepted.
std::array< std::uint8_t, uuidLenght > arUuid
std::size_t PrmBeginCount() const noexcept
Return the number of PrmBegin requests accepted.
void ScheduleApplicationReady()
Start a cancellable ApplicationReady delay.
void SendApplicationReady()
Send a device-originated ApplicationReady request.
std::uint16_t RpcPort() const noexcept
Return the configured device RPC port.
std::size_t prmBeginCount
std::unique_ptr< asio::ip::udp::socket > socket
std::size_t ApplicationReadyResponseCount() const noexcept
Return the number of ApplicationReady responses accepted.
std::optional< asio::ip::udp::endpoint > controllerEndpoint
void Stop() noexcept
Stop listening and join the worker thread.
void Start()
Start listening for controller RPC packets.
std::atomic< bool > running
static Bytes MakeAlarmCrResponse(std::uint16_t deviceAlarmReference)
Build a minimal successful AlarmCRBlockRes.
std::condition_variable stateCondition
void Run()
Worker loop receiving controller datagrams.
std::thread applicationReadyWorker
Delayed ApplicationReady worker.
std::optional< PNRPCHeader > lastApplicationReadyRequest
std::unique_ptr< asio::io_context > ioContext
std::size_t applicationReadyRequestCount
bool SawInputIocr() const noexcept
Return whether the controller supplied an Input IOCR.
void HandleApplicationReadyResponse(const PNRPCHeader &response)
Handle the response to the device-originated ApplicationReady.
bool sawExpectedSubmodules
void HandleControl(const PNRPCHeader &request, const asio::ip::udp::endpoint &sender)
Handle a CONTROL request such as PrmBegin or PrmEnd.
VirtualProfinetRpcDevice(VirtualProfinetDeviceConfiguration config={})
Construct a stopped simulator.
std::size_t applicationReadyResponseCount
std::size_t ConnectCount() const noexcept
Return the number of CONNECT requests accepted.
~VirtualProfinetRpcDevice()
Stop the simulator.
std::size_t ApplicationReadyRequestCount() const noexcept
Return the number of ApplicationReady requests sent.
static void ParseArIdentity(const Bytes &connectPayload, std::array< std::uint8_t, uuidLenght > &arUuid, std::uint16_t &sessionKey)
Parse the AR UUID and session key from a CONNECT request.
VirtualProfinetDeviceState State() const noexcept
Return the current protocol state.
static Bytes MakeIocrResponse(const PNIOCRBlockReqHeader &request, std::uint16_t frameId)
Build one IOCRBlockRes from a controller IOCR request.
std::string LastError() const
Return the last diagnostic/rejection text.
VirtualProfinetDeviceConfiguration config
bool ValidateConnect(const Bytes &nrdPayload)
Validate and extract the CONNECT blocks.
void HandlePacket(const PNRPCHeader &request, const asio::ip::udp::endpoint &sender)
Dispatch one complete RPC packet.
VirtualProfinetDeviceState state
void SendResponse(const PNRPCHeader &request, const Bytes &nrdPayload, const asio::ip::udp::endpoint &sender)
Send a successful RPC response.
std::size_t ReleaseCount() const noexcept
Return the number of Release requests observed.
bool SawExpectedSubmodules() const noexcept
Return whether the controller supplied ExpectedSubmodule blocks.
static Bytes MakeNrd(const Bytes &payload)
Build an NRD wrapper with successful status.
void HandleConnect(const PNRPCHeader &request, const asio::ip::udp::endpoint &sender)
Handle a CONNECT request.
static PNRPCHeader MakeResponse(const PNRPCHeader &request, const Bytes &nrdPayload)
Build an RPC response preserving correlation fields.
bool SawAlarmCr() const noexcept
Return whether the controller supplied an AlarmCR.
bool SawOutputIocr() const noexcept
Return whether the controller supplied an Output IOCR.
bool WaitForState(VirtualProfinetDeviceState expected, std::chrono::milliseconds timeout) const
Wait until the simulator reaches a state.
Lightweight cursor for parsing a big-endian byte buffer without copies.
std::array< std::uint8_t, N > Fixed()
Read and consume a fixed-size byte array.
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.
@ LastFragment
Bit 1 (0x02): Final fragment in the sequence.
@ Idempotent
Bit 5 (0x20): Activity call is idempotent (safe to retry).
@ NoFack
Bit 3 (0x08): Do not send FACK (fragment acknowledgment) frames.
constexpr std::uint8_t VERSION_HIGH
Version high.
constexpr std::uint16_t CONTROL_CMD_PRM_BEGIN
Control command bit: PrmBegin.
constexpr std::uint16_t CONTROL_CMD_PRM_END
Control command bit: PrmEnd.
BlockHeaderView PeekBlockHeader(const Bytes &data, std::size_t offset)
constexpr std::uint8_t VERSION_LOW
Version low.
VirtualProfinetDeviceState
Observable Application Relation state of the virtual device.
@ ApplicationReady
The controller confirmed the device's ApplicationReady request.
@ Connected
A CONNECT was accepted and the AR is being configured.
@ Fault
The simulator rejected a request.
@ Parameterizing
PrmBegin was accepted.
@ Parameterized
PrmEnd was accepted and the device can issue ApplicationReady.
@ Idle
No Application Relation exists.
@ Released
The controller released the AR.
const char * ToString(VirtualAumaScenario scenario) noexcept
Convert a virtual AUMA scenario into its stable CLI spelling.
void PutU8(std::vector< std::uint8_t > &out, std::uint8_t value)
Append a single byte to a buffer.
void PutU16(std::vector< std::uint8_t > &out, std::uint16_t value)
Append a 16-bit value to a buffer in big-endian order.
BlockType
PROFINET block types.
@ ControlPrmEndRequest
IOD Control Prm End request.
@ ApplicationRelationRequest
AR block request.
@ ExpectedSubmoduleRequest
Expected submodule block request.
@ ReleaseRequest
IOD Release request.
@ AlarmCrResponse
AlarmCR block response.
@ ControlApplicationReadyRequest
IOD Control Application Ready request.
@ ParameterizationBeginRequest
Parameterization Begin request.
constexpr int uuidLenght
Constant lenght of a UUID.
@ ApplicationReady
Application Ready command.
std::vector< std::uint8_t > Bytes
Generic byte buffer alias used throughout the library for raw wire data.
static constexpr BlockType BLOCK_TYPE
Block type identifier for AlarmCRBlockReq.
AlarmCRBlockRes: device's response confirming the alarm connection (part of the CONNECT response).
Bytes ToBytes() const
Serialize this block back to raw bytes.
std::array< std::uint8_t, blockHeaderLength > blockHeader
Nested 6-byte block header.
static constexpr std::size_t kSize
Size in bytes of this block.
std::uint16_t localAlarmReference
Local alarm reference assigned by the device.
static constexpr std::uint16_t BLOCK_TYPE
Block type identifier for IOCRBlockRes.
static constexpr std::size_t kSize
Size in bytes of this block.
Control block used for Release/PrmBegin/PrmEnd/ApplicationReady/RTClass3 requests.
std::array< std::uint8_t, uuidLenght > arUuid
AR UUID this control operation applies to.
std::uint16_t controlBlockProperties
Control block property flags.
std::uint16_t padding1
Reserved/padding.
std::uint16_t sessionKey
Session key matching the AR.
std::array< std::uint8_t, blockHeaderLength > blockHeader
Nested 6-byte block header.
ControlCommand controlCommand
Control command bitmask (PrmEnd/AppReady/Release/Done/...).
std::uint16_t padding2
Reserved/padding.
Bytes ToBytes() const
Serialize this block back to raw bytes.
NRD (Network Representation Data) wrapper carrying the actual IOD payload.
Bytes payload
The wrapped IOD data.
static PNNRDData Parse(const Bytes &data)
Parse an NRD wrapper and payload from raw bytes.
Bytes ToBytes() const
Serialize this wrapper and payload back to raw bytes.
std::uint32_t argsLength
Length in bytes of the arguments (usually equals ActualCount).
std::uint32_t offset
Byte offset into the logical result (0 unless fragmented).
std::uint32_t argsMaximumStatus
Maximum status/argument buffer size accepted by the caller.
std::uint32_t maximumCount
Maximum number of bytes the caller can accept.
std::uint32_t actualCount
Actual length in bytes of Payload.
Configuration for a controller-facing virtual PROFINET device.
bool requireExpectedSubmodules
Require ExpectedSubmoduleBlockReq in cyclic CONNECT requests.
std::chrono::milliseconds applicationReadyDelay
Delay between accepting PrmEnd and sending ApplicationReady.
bool automaticApplicationReady
Automatically send ApplicationReady after PrmEnd.
bool requireBothIocrs
Require an Input and Output IOCR when CONNECT contains cyclic data.
std::string controllerAddress
Controller loopback address used for ApplicationReady.
std::uint16_t outputFrameId
Frame ID returned for the output IOCR.
std::uint16_t inputFrameId
Frame ID returned for the input IOCR.
bool requireAlarmCr
Require AlarmCR when cyclic CONNECT is requested.
std::uint16_t controllerCcontrolPort
Controller CControl listener port used for ApplicationReady.
std::uint16_t rpcPort
UDP port on which the virtual device accepts controller RPC.
std::uint16_t deviceAlarmReference
Alarm reference returned by the device when AlarmCR is supported.
std::uint16_t frameId
Ethernet FrameID.
std::unique_ptr< cyclic::CyclicController > controller
Stateful PROFINET IO-Device DCE/RPC lifecycle simulator.
Generic helpers for reading and writing integer and byte data in big- and little-endian wire formats.