9#include <gtest/gtest.h>
34constexpr std::uint16_t kSendClockFactor = 32;
35constexpr std::uint16_t kReductionRatio = 8;
36constexpr std::uint16_t kInputLength = 43;
37constexpr std::uint16_t kOutputLength = 40;
38constexpr std::uint16_t kAumaInputLength = 34;
39constexpr std::uint16_t kAumaOutputLength = 16;
40constexpr std::uint16_t kAumaInputOffset = 1;
41constexpr std::uint16_t kAumaOutputOffset = 8;
42constexpr std::uint16_t kAumaInputIopsOffset = 35;
43constexpr std::uint8_t kOpen = 0x01;
44constexpr std::uint8_t kNotOpen = 0x00;
46constexpr MacAddress kIoControllerMac{0xA8, 0x74, 0x1D, 0x44, 0x7A, 0xBB};
47constexpr MacAddress kAumaMac{0xA0, 0xD3, 0x85, 0x00, 0xF6, 0x13};
49std::filesystem::path Fixture(
const std::string& name)
51 const std::array<std::filesystem::path, 4> paths{
55 std::filesystem::current_path() /
"tests" /
"gsdml" / name};
57 for (
const auto& path : paths)
59 if (std::filesystem::exists(path))
65 ADD_FAILURE() <<
"GSDML fixture not found: " << name;
71 const auto path = Fixture(name);
75const rpc::IOSlot* Slot(
const std::vector<rpc::IOSlot>& slots, std::uint16_t slot, std::uint16_t subslot)
77 const auto it = std::ranges::find_if(slots, [slot, subslot](
const rpc::IOSlot& value)
79 return value.
slot == slot && value.
subslot == subslot;
81 return it == slots.end() ? nullptr : &(*it);
84std::vector<Bytes> Blocks(
const Bytes& data)
86 std::vector<Bytes> result;
87 std::size_t offset = 0;
91 const std::size_t size = 4U + header.blockLength;
92 if (size > data.size() - offset)
96 result.emplace_back(data.begin() + offset, data.begin() + offset + size);
114 {.slot = 0, .subslot = 0x0001, .frameOffset = 0, .dataLength = 0, .offsetIOCS = 0},
115 {.slot = 0, .subslot = 0x0002, .frameOffset = 1, .dataLength = 0, .offsetIOCS = 1},
116 {.slot = 0, .subslot = 0x0003, .frameOffset = 2, .dataLength = 0, .offsetIOCS = 2},
117 {.slot = 0, .subslot = 0x0004, .frameOffset = 3, .dataLength = 0, .offsetIOCS = 3},
118 {.slot = 0, .subslot = 0x8000, .frameOffset = 4, .dataLength = 0, .offsetIOCS = 4},
119 {.slot = 0, .subslot = 0x8001, .frameOffset = 5, .dataLength = 0, .offsetIOCS = 5},
120 {.slot = 0, .subslot = 0x8002, .frameOffset = 6, .dataLength = 0, .offsetIOCS = 6},
121 {.slot = 1, .subslot = 1, .frameOffset = kAumaInputOffset, .dataLength = kAumaInputLength, .offsetIOPS = kAumaInputIopsOffset},
122 {.slot = 2, .subslot = 1, .frameOffset = 0, .dataLength = 0, .offsetIOCS = 42}};
137 c.
objects = {{.slot = 0, .subslot = 0x0001, .frameOffset = 0, .dataLength = 0, .offsetIOCS = 0},
138 {.slot = 0, .subslot = 0x0002, .frameOffset = 1, .dataLength = 0, .offsetIOCS = 1},
139 {.slot = 0, .subslot = 0x0003, .frameOffset = 2, .dataLength = 0, .offsetIOCS = 2},
140 {.slot = 0, .subslot = 0x0004, .frameOffset = 3, .dataLength = 0, .offsetIOCS = 3},
141 {.slot = 0, .subslot = 0x8000, .frameOffset = 4, .dataLength = 0, .offsetIOCS = 4},
142 {.slot = 0, .subslot = 0x8001, .frameOffset = 5, .dataLength = 0, .offsetIOCS = 5},
143 {.slot = 0, .subslot = 0x8002, .frameOffset = 6, .dataLength = 0, .offsetIOCS = 6},
144 {.slot = 1, .subslot = 1, .frameOffset = 7, .dataLength = 0, .offsetIOCS = 7},
145 {.slot = 2, .subslot = 1, .frameOffset = kAumaOutputOffset, .dataLength = kAumaOutputLength, .offsetIOPS = 24}};
149Bytes InputPayload(std::uint16_t position)
184 payload[3] =
static_cast<std::uint8_t
>(position >> 8U);
185 payload[4] =
static_cast<std::uint8_t
>(position);
186 payload[kAumaInputIopsOffset] = 0x80;
194 Bytes SendReceive(
const Bytes& request, std::chrono::milliseconds )
override
215 nrd.
argsLength =
static_cast<std::uint32_t
>(blocks.size());
218 nrd.
actualCount =
static_cast<std::uint32_t
>(blocks.size());
225 void Send(
const Bytes& data)
override
227 sends.push_back(data);
229 void Open(
const std::string& , std::uint16_t ,
bool )
override
232 Bytes Receive(std::chrono::milliseconds , asio::ip::udp::endpoint& )
override
236 void SendTo(
const Bytes& data,
const asio::ip::udp::endpoint& )
override
238 sends.push_back(data);
240 void Close() noexcept
override
251 info.
name =
"AUMA-SGx";
252 info.ip =
"127.0.0.1";
253 info.vendorHigh = 0x01;
254 info.vendorLow = 0x3F;
255 info.deviceHigh = 0x00;
256 info.deviceLow = 0x02;
262TEST(IoControllerAumaGsdml, AumaProfileMatchesCaptureDevice)
264 const auto auma = Load(
"GSDML-V2.42-AUMA-SGX-20220920.xml");
265 EXPECT_EQ(auma.vendorId, 0x013F);
266 EXPECT_EQ(auma.deviceId, 0x0002);
267 EXPECT_EQ(auma.dapModuleIdentNumber, 0x80010000U);
269 const auto slots = auma.BuildIoSlots();
270 const auto* input = Slot(slots, 1, 1);
271 const auto* output = Slot(slots, 2, 1);
272 ASSERT_NE(input,
nullptr);
273 ASSERT_NE(output,
nullptr);
274 EXPECT_EQ(input->moduleIdent, 0x00008000U);
275 EXPECT_EQ(input->submoduleIdent, 0x00002200U);
276 EXPECT_EQ(input->inputLength, kAumaInputLength);
277 EXPECT_EQ(output->moduleIdent, 0x00008100U);
278 EXPECT_EQ(output->submoduleIdent, 0x10001000U);
279 EXPECT_EQ(output->outputLength, kAumaOutputLength);
282TEST(IoControllerAumaGsdml, PhoenixProfileMatchesCaptureController)
284 const auto phoenix = Load(
"GSDML-V2.45-Phoenix Contact-AXC F 1252-20250813.xml");
285 EXPECT_EQ(phoenix.vendorId, 0x00B0);
286 EXPECT_EQ(phoenix.deviceId, 0x0193);
287 EXPECT_EQ(phoenix.dapModuleIdentNumber, 0x00000300U);
288 EXPECT_NE(phoenix.FindModule(
"PNSlot1"),
nullptr);
291TEST(IoControllerAumaConnection, ConnectUsesCapturedNegotiatedFrameIdsAndTiming)
293 auto transport = std::make_unique<ConnectCapture>();
294 auto* capture = transport.get();
295 auto control = std::make_unique<ConnectCapture>();
297 rpc::RPCCon connection(AumaInfo(), {}, std::move(transport), std::move(control));
298 const auto auma = Load(
"GSDML-V2.42-AUMA-SGX-20220920.xml");
300 setup.
slots = auma.BuildIoSlots();
301 setup.sendClockFactor = kSendClockFactor;
302 setup.reductionRatio = kReductionRatio;
303 setup.watchdogFactor = 3;
304 setup.dataHoldFactor = 6;
306 const auto result = connection.Connect(kIoControllerMac,
true, setup);
307 ASSERT_TRUE(result.has_value());
310 ASSERT_EQ(capture->requests.size(), 1U);
314 const auto blocks = Blocks(nrd.
payload);
315 ASSERT_GE(blocks.size(), 4U);
324 EXPECT_EQ(input.iocrType, 1);
325 EXPECT_EQ(input.iocrReference, 1);
326 EXPECT_EQ(input.dataLength, kInputLength);
327 EXPECT_EQ(input.sendClockFactor, kSendClockFactor);
328 EXPECT_EQ(input.reductionRatio, kReductionRatio);
329 EXPECT_EQ(output.iocrType, 2);
330 EXPECT_EQ(output.iocrReference, 2);
331 EXPECT_EQ(output.dataLength, kOutputLength);
332 EXPECT_EQ(output.sendClockFactor, kSendClockFactor);
333 EXPECT_EQ(output.reductionRatio, kReductionRatio);
335 const auto types = std::vector<std::uint16_t>{
339 const std::vector<std::uint16_t> expected = {
343 EXPECT_EQ(types, expected);
346TEST(IoControllerAumaCyclicIo, OutputOpenAndStopUseCapturedByte25)
348 const auto config = CapturedOutput();
363 auto openPayload = builder.
Build();
364 ASSERT_EQ(openPayload.size(), kOutputLength);
365 EXPECT_EQ(openPayload[kAumaOutputOffset], kOpen);
369 const auto stopOpenPayload = builder.
Build();
370 ASSERT_EQ(stopOpenPayload.size(), kOutputLength);
372 EXPECT_EQ(stopOpenPayload[kAumaOutputOffset], kNotOpen);
379 .payload = openPayload};
387 auto etherFrametype = rh.
U16();
396 auto dapIocr = r.
U8();
397 auto prmIocr = r.
U8();
398 auto pzsIocr = r.
U8();
399 auto fileTransferIocr = r.
U8();
400 auto interfaceIocr = r.
U8();
401 auto port1Iocr = r.
U8();
402 auto port2Iocr = r.
U8();
403 auto inputIocr = r.
U8();
404 auto outputbytes = r.
Fixed<kAumaOutputLength>();
406 auto outputIops = r.
U8();
419 auto commands = outputProcessDataReader.
U8();
420 auto remainingOutputBytes = outputProcessDataReader.
ReadBytes(outputProcessDataReader.
Remaining());
422 ASSERT_EQ(ethernet.size(), 60U);
426 EXPECT_TRUE(commands & kOpen);
428 for (
const auto remainingOutputByte : remainingOutputBytes)
430 EXPECT_EQ(remainingOutputByte, 0x00);
438 .payload = stopOpenPayload};
446 etherFrametype = rhStopOpen.
U16();
458 fileTransferIocr = r2.
U8();
459 interfaceIocr = r2.
U8();
463 outputbytes = r2.
Fixed<kAumaOutputLength>();
465 outputIops = r2.
U8();
468 wire::Reader outputProcessDataReaderStopOpen(outputbytes);
478 commands = outputProcessDataReaderStopOpen.
U8();
479 remainingOutputBytes = outputProcessDataReaderStopOpen.
ReadBytes(outputProcessDataReaderStopOpen.
Remaining());
481 ASSERT_EQ(ethernet.size(), 60U);
485 EXPECT_FALSE(commands & kOpen);
487 for (
const auto remainingOutputByte : remainingOutputBytes)
489 EXPECT_EQ(remainingOutputByte, 0x00);
493TEST(IoControllerAumaCyclicIo, InputPositionSamplesRemainBigEndianAtCapturedBytes20And21)
495 auto config = CapturedInput();
498 constexpr std::array<std::uint16_t, 6> samples{
499 0x0202, 0x0206, 0x0207, 0x020F, 0x0217, 0x0221};
501 for (
const auto sample : samples)
503 builder.
Load(InputPayload(sample));
508 auto logicalSignals = r.
U8();
509 auto actuatorSignals = r.
U8();
510 auto actualPosition = r.
U16();
512 ASSERT_EQ(
payload.size(), kInputLength);
513 EXPECT_EQ(actualPosition, sample);
514 EXPECT_EQ(
payload[kAumaInputIopsOffset], 0x80);
515 EXPECT_EQ(iOps, 0x80);
519TEST(IoControllerAumaCyclicIo, Layer2FramesMatchCaptureLengthsAndDirections)
522 kAumaMac, kIoControllerMac,
528 .payload =
Bytes(kOutputLength, 0)});
530 kIoControllerMac, kAumaMac,
536 .payload =
Bytes(kInputLength, 0x80)});
538 EXPECT_EQ(output.size(), 60U);
539 EXPECT_EQ(input.size(), 63U);
544 auto etherFrametypeOutput = roh.
U16();
554 auto etherFrametypeInput = rih.
U16();
562TEST(IoControllerAumaCyclicIo, CapturedOneSecondOpenSequenceUsesEightMillisecondCycle)
564 constexpr std::size_t kOpenFirstPacket = 431;
565 constexpr std::size_t kStopFirstPacket = 709;
566 constexpr std::size_t kFirstOpenPacket = 431;
567 constexpr std::size_t kLastOpenPacket = 707;
568 constexpr std::int64_t kObservedMicroseconds = 999951;
570 EXPECT_EQ(kStopFirstPacket - kFirstOpenPacket, 278U);
571 EXPECT_EQ(kLastOpenPacket - kFirstOpenPacket + 1U, 277U);
572 EXPECT_EQ(kObservedMicroseconds / 125000, 7);
573 EXPECT_NE(kOpenFirstPacket, 0U);
574 EXPECT_EQ(kSendClockFactor * kReductionRatio * 125U / 4U, 8000U);
Parsed PROFINET device information from a DCP response.
std::string name
Station name.
Connection to a PROFINET IO-Device: establishes an AR (Application Relationship) and performs acyclic...
Builds C_SDU payload from IO data objects with double-buffering.
void Load(const Bytes &payload)
Load received payload data into the write buffer.
void SetData(int slot, int subslot, const Bytes &data)
Write one object's data into the write buffer.
void Swap()
Promote the write buffer to the send buffer.
Bytes Build() const
Get the current send buffer contents.
Lightweight cursor for parsing a big-endian byte buffer without copies.
std::uint8_t U8()
Read and consume one byte.
std::size_t Remaining() const
Number of bytes not yet consumed.
std::array< std::uint8_t, N > Fixed()
Read and consume a fixed-size byte array.
std::vector< std::uint8_t > ReadBytes(std::size_t n)
Read and consume a variable-length byte range.
std::uint16_t U16()
Read and consume a big-endian 16-bit value.
Declares the GSDML parser and PROFINET device-description data model.
GSDMLDevice ParseGsdml(const std::string &path)
Parse a GSDML file into a GSDMLDevice.
constexpr std::uint8_t VERSION_HIGH
Version high.
BlockHeaderView PeekBlockHeader(const Bytes &data, std::size_t offset)
constexpr std::uint8_t VERSION_LOW
Version low.
constexpr std::uint16_t IOCR_TYPE_INPUT
IOCR direction: Device -> Controller.
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::uint8_t DATA_STATUS_VALID
DataStatus bit: data validity, 0=Invalid, 1=Valid.
constexpr std::uint16_t IOCR_TYPE_OUTPUT
IOCR direction: Controller -> Device.
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.
@ ApplicationRelationRequest
AR block request.
TEST(AlarmEndpointTests, DefaultTransportIsLayer2)
constexpr int blockHeaderLength
Constant lenght of a blockHeader.
std::array< std::uint8_t, macAddressLength > MacAddress
A 6-byte Ethernet MAC address.
std::filesystem::path GetExecutableDirectory()
Retrieves the absolute directory path of the currently running executable.
constexpr int macAddressLength
Constant lenght of a mac address.
constexpr std::uint16_t PROFINET_ETHERTYPE
EtherType value identifying PROFINET frames (0x8892).
constexpr std::uint16_t kInputFrameId
std::vector< std::uint8_t > Bytes
Generic byte buffer alias used throughout the library for raw wire data.
constexpr std::uint16_t kOutputFrameId
PROFINET protocol wire-format structures and serialization helpers.
PROFINET DCE/RPC protocol structures and IO-Device connection API.
Declares PROFINET RT_CLASS_1 cyclic frame and IOCR data structures.
static constexpr std::uint16_t BLOCK_TYPE
Block type identifier for IOCRBlockRes.
static constexpr std::size_t kSize
Size in bytes of this block.
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.
Complete parsed GSDML device profile.
Configuration for establishing cyclic IO alongside an AR.
std::vector< IOSlot > slots
Slots to include in the cyclic data frames.
One slot/subslot's expected module configuration and IO data sizes.
std::uint16_t slot
Slot number.
std::uint16_t subslot
Subslot number.
IOCR configuration derived from AR setup: timing parameters and IO object mappings needed for cyclic ...
std::vector< IODataObject > objects
IO data objects carried in this IOCR's cyclic frame.
std::uint16_t dataLength
Total cyclic data length in bytes.
std::uint16_t typeIOCR
IOCR direction: 1=Input, 2=Output.
std::uint16_t referenceIOCR
IOCR reference assigned during AR establishment.
std::uint16_t sendClockFactor
Send clock base factor (31.25us units).
std::uint16_t frameId
Frame ID assigned to this IOCR.
std::uint16_t reductionRatio
Reduction ratio relative to the send clock.
std::uint16_t watchdogFactor
Watchdog factor (missed-frame tolerance before a timeout fault).
std::uint16_t phase
Phase offset within the reduction ratio.
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::uint16_t frameId
Ethernet FrameID.
std::vector< Bytes > sends
std::vector< Bytes > requests