PROFINET IO Controller Stack 1.0.0
Modern C++ implementation of a PROFINET IO Controller stack
Loading...
Searching...
No Matches
test_dcp_asioIntegration.cpp
Go to the documentation of this file.
1
11
12#include <gtest/gtest.h>
13
14#include <array>
15#include <asio.hpp>
16#include <cstdint>
17#include <exception>
18#include <string>
19#include <thread>
20#include <vector>
21
24#include "profinet/dcp.h"
25#include "profinet/exceptions.h"
26#include "profinet/protocol.h"
27
28using namespace profinet;
29using namespace profinet::dcp;
30using namespace std::chrono_literals;
31#ifndef PROFINET_TEST_INTERFACE
32#define PROFINET_TEST_INTERFACE ""
33#endif
34namespace
35{
36[[nodiscard]] std::string GetIntegrationTestInterface()
37{
38 // const char* interfaceName = std::getenv("PROFINET_TEST_INTERFACE");
39
40 // return interfaceName != nullptr ? interfaceName : std::string{};
42}
43
44const MacAddress kControllerMac{0x08, 0x00, 0x27, 0x31, 0x39, 0xDC};
45
46const MacAddress kDeviceMac{0xA0, 0xD3, 0x85, 0x00, 0xF6, 0x13};
47
56Bytes BuildDcpResponseFrame(const MacAddress& destination, const MacAddress& source, std::uint32_t xid, const Bytes& block, bool vlan = false)
57{
58 PNDCPHeader dcp;
59 dcp.frameId = DcpFrameId::GetSet;
60 dcp.serviceId = DcpServiceId::Get;
61 dcp.serviceType = DcpServiceType::ResponseSuccess;
62 dcp.xId = xid;
63 dcp.respDelay = 0;
64 dcp.length = static_cast<std::uint16_t>(block.size());
65 dcp.payload = block;
66
67 const Bytes dcpBytes = dcp.ToBytes();
68
70 frame.reserve(14U + (vlan ? 4U : 0U) + dcpBytes.size());
71
72 frame.insert(frame.end(), destination.begin(), destination.end());
73
74 frame.insert(frame.end(), source.begin(), source.end());
75
76 if (vlan)
77 {
78 frame.insert(frame.end(), {
79 0x81, 0x00, // 802.1Q TPID.
80 0xC0, 0x00, // Priority-tagged VLAN TCI.
81 0x88, 0x92 // Inner EtherType: PROFINET.
82 });
83 }
84 else
85 {
86 frame.insert(frame.end(), {
87 0x88, 0x92 // PROFINET EtherType.
88 });
89 }
90
91 frame.insert(frame.end(), dcpBytes.begin(), dcpBytes.end());
92
93 return frame;
94}
95
100Bytes BuildStationNameBlock(const std::string& stationName)
101{
102 const Bytes nameBytes(stationName.begin(), stationName.end());
103
104 PNDCPBlockRequest block;
105 block.option = DcpOption::Device;
106 block.suboption = static_cast<std::uint8_t>(DcpDeviceSuboption::Name);
107 block.length = static_cast<std::uint16_t>(nameBytes.size());
108 block.payload = nameBytes;
109
110 Bytes result = block.ToBytes();
111
112 if ((nameBytes.size() % 2U) != 0U)
113 {
114 result.push_back(0x00);
115 }
116
117 return result;
118}
119
125template <typename Predicate>
126Bytes ReceiveMatchingFrame(AsioEthernetSocket& socket, Predicate&& predicate)
127{
128 socket.SetTimeout(100ms);
129
130 for (int attempt = 0; attempt < 10; ++attempt)
131 {
132 const Bytes frame = socket.Recv();
133
134 if (frame.empty())
135 {
136 return {};
137 }
138
139 if (predicate(frame))
140 {
141 return frame;
142 }
143 }
144
145 return {};
146}
147
149class DcpAsioIntegrationTests : public ::testing::Test
150{
151 protected:
153 void SetUp() override
154 {
155 testInterface = GetIntegrationTestInterface();
156
157 if (testInterface.empty())
158 {
159 GTEST_SKIP() << "PROFINET_TEST_INTERFACE is not set. "
160 << "Set it before starting VS Code, for example: "
161 << "export PROFINET_TEST_INTERFACE=enp0s8";
162 }
163 ASSERT_NO_THROW(socket = std::make_unique<AsioEthernetSocket>(ioContext, testInterface, 0));
164
165 ASSERT_NO_THROW(peerSocket = std::make_unique<AsioEthernetSocket>(ioContext, testInterface, 0));
166 }
167
169 void TearDown() override
170 {
171 peerSocket.reset();
172 socket.reset();
173 }
174
176 asio::io_context ioContext;
177
180
182 std::unique_ptr<AsioEthernetSocket> socket;
183
185 std::unique_ptr<AsioEthernetSocket> peerSocket;
186
188 std::string testInterface;
189};
190
191} // namespace
192
195TEST_F(DcpAsioIntegrationTests, SendDiscoverTransmitsIdentifyRequest)
196{
197 dcp::SendDiscover(*socket, kControllerMac);
198
199 const Bytes frame = ReceiveMatchingFrame(*peerSocket, [](const Bytes& data)
200 {
201 if (data.size() < 14) // EthernetHeader::kFixedSize
202 {
203 return false;
204 }
205
206 try
207 {
208 const EthernetHeader ethernet = EthernetHeader::Parse(data);
209
210 if (ethernet.dst != String2Mac(DCP_MULTICAST_MAC))
211 {
212 return false;
213 }
214
215 if (ethernet.src != kControllerMac)
216 {
217 return false;
218 }
219
220 if (ethernet.type != PROFINET_ETHERTYPE)
221 {
222 return false;
223 }
224
225 const PNDCPHeader dcp = PNDCPHeader::Parse(ethernet.payload);
226
227 return dcp.frameId == DcpFrameId::IdentifyRequest &&
228 dcp.serviceId == DcpServiceId::Identify &&
229 dcp.serviceType == DcpServiceType::Request &&
230 dcp.respDelay == DEFAULT_RESPONSE_DELAY_FACTOR;
231 }
232 catch (const std::exception&)
233 {
234 return false;
235 }
236 });
237
238 ASSERT_FALSE(frame.empty());
239
241
242 EXPECT_EQ(ethernet.dst, String2Mac(DCP_MULTICAST_MAC));
243 EXPECT_EQ(ethernet.src, kControllerMac);
244 EXPECT_EQ(ethernet.type, PROFINET_ETHERTYPE);
245
246 const PNDCPHeader dcp = PNDCPHeader::Parse(ethernet.payload);
247
248 EXPECT_EQ(dcp.frameId, DcpFrameId::IdentifyRequest);
249
250 EXPECT_EQ(dcp.serviceId, DcpServiceId::Identify);
251
252 EXPECT_EQ(dcp.serviceType, DcpServiceType::Request);
253
255
256 EXPECT_NE(dcp.xId, 0U);
257}
258
261TEST_F(DcpAsioIntegrationTests, ReadResponseParsesAsioEthernetFrame)
262{
263 const Bytes block = BuildStationNameBlock("AUMA");
264
265 const std::uint32_t xid = 0x12345678U;
266
267 const Bytes response = BuildDcpResponseFrame(kControllerMac, kDeviceMac, xid, block);
268
269 peerSocket->Send(response);
270
271 const ResponseMap result = dcp::ReadResponse(*socket, kControllerMac, 1.0, true, xid);
272
273 ASSERT_EQ(result.size(), 1U);
274
275 const auto deviceIt = result.find(kDeviceMac);
276
277 ASSERT_NE(deviceIt, result.end());
278
279 const auto blockIt = deviceIt->second.find(MakeBlockKey(DcpOption::Device, static_cast<std::uint8_t>(DcpDeviceSuboption::Name)));
280
281 ASSERT_NE(blockIt, deviceIt->second.end());
282
283 EXPECT_EQ(blockIt->second, (Bytes{'A', 'U', 'M', 'A'}));
284}
285
288TEST_F(DcpAsioIntegrationTests, ReadResponseParsesVlanTaggedAsioEthernetFrame)
289{
290 const Bytes block = BuildStationNameBlock("AUMA");
291
292 const std::uint32_t xid = 0x23456789U;
293
294 const Bytes response = BuildDcpResponseFrame(kControllerMac, kDeviceMac, xid, block, true);
295
296 peerSocket->Send(response);
297
298 const ResponseMap result = dcp::ReadResponse(*socket, kControllerMac, 1.0, true, xid);
299
300 ASSERT_EQ(result.size(), 1U);
301
302 const auto deviceIt = result.find(kDeviceMac);
303
304 ASSERT_NE(deviceIt, result.end());
305
306 const auto blockIt = deviceIt->second.find(MakeBlockKey(DcpOption::Device, static_cast<std::uint8_t>(DcpDeviceSuboption::Name)));
307
308 ASSERT_NE(blockIt, deviceIt->second.end());
309
310 EXPECT_EQ(blockIt->second, (Bytes{'A', 'U', 'M', 'A'}));
311}
312
322TEST_F(DcpAsioIntegrationTests, GetParamPerformsAsioRoundTrip)
323{
324 std::exception_ptr peerException;
325 Bytes receivedRequest;
326
327 std::jthread peerWorker([&]
328 {
329 try
330 {
331 peerSocket->SetTimeout(1000ms);
332
333 receivedRequest = peerSocket->Recv();
334
335 if (receivedRequest.empty())
336 {
337 throw std::runtime_error("DCP GET request was not received");
338 }
339
340 const EthernetHeader requestEthernet = EthernetHeader::Parse(receivedRequest);
341
342 EXPECT_EQ(requestEthernet.dst, kDeviceMac);
343
344 EXPECT_EQ(requestEthernet.src, kControllerMac);
345
346 EXPECT_EQ(requestEthernet.type, PROFINET_ETHERTYPE);
347
348 const PNDCPHeader requestDcp = PNDCPHeader::Parse(requestEthernet.payload);
349
350 EXPECT_EQ(requestDcp.frameId, DcpFrameId::GetSet);
351
352 EXPECT_EQ(requestDcp.serviceId, DcpServiceId::Get);
353
354 EXPECT_EQ(requestDcp.serviceType, DcpServiceType::Request);
355
356 EXPECT_EQ(requestDcp.length, 2U); // need to check specs!!
357
358 EXPECT_EQ(requestDcp.payload.size(), 4U);
359 EXPECT_EQ(requestDcp.payload[0], static_cast<std::uint8_t>(DcpOption::Device));
360
361 EXPECT_EQ(requestDcp.payload[1], static_cast<std::uint8_t>(DcpDeviceSuboption::Name));
362
363 EXPECT_EQ(requestDcp.payload[2], 0x00); // block length = 0
364 EXPECT_EQ(requestDcp.payload[3], 0x00);
365
366 const Bytes responseBlock = BuildStationNameBlock("AUMA");
367
368 const Bytes response = BuildDcpResponseFrame(kControllerMac, kDeviceMac, requestDcp.xId, responseBlock);
369
370 peerSocket->Send(response);
371 }
372 catch (...)
373 {
374 peerException = std::current_exception();
375 }
376 });
377
378 const auto result = dcp::GetParam(*socket, kControllerMac, Mac2String(kDeviceMac), "name", 1);
379
380 peerWorker.join();
381
382 if (peerException)
383 {
384 std::rethrow_exception(peerException);
385 }
386
387 ASSERT_FALSE(receivedRequest.empty());
388 ASSERT_TRUE(result.has_value());
389
390 EXPECT_EQ(*result, (Bytes{'A', 'U', 'M', 'A'}));
391}
Runs an Asio I/O context on a dedicated worker thread.
Asio-backed platform-independent raw Ethernet transport.
Platform-independent raw Ethernet transport using Asio.
std::vector< std::uint8_t > Recv() override
Receive one complete Ethernet frame.
void SetTimeout(std::chrono::milliseconds timeout) override
Set the receive timeout.
Keeps an Asio I/O context running on a dedicated worker thread.
PROFINET DCP discovery and device configuration interface.
Declares exceptions and error types used by the PROFINET IO controller stack.
void SendDiscover(IRawEthernetSocket &sock, const MacAddress &src, std::uint16_t responseDelay=DEFAULT_RESPONSE_DELAY_FACTOR)
Send a DCP Identify multicast request to discover all devices on the segment.
Definition dcp.cpp:1173
std::optional< std::vector< std::uint8_t > > GetParam(IRawEthernetSocket &sock, const MacAddress &src, const std::string &target, const std::string &param, int timeoutSec=DEFAULT_TIMEOUT)
Read a named parameter from a device.
Definition dcp.cpp:1005
constexpr DcpBlockKey MakeBlockKey(std::uint16_t option, std::uint8_t suboption)
Definition dcp.h:231
constexpr std::uint16_t DEFAULT_RESPONSE_DELAY_FACTOR
Default dcp response delay factor.
Definition dcp.h:70
const std::string DCP_MULTICAST_MAC
DCP Identify/Get/Set multicast destination MAC.
Definition dcp.h:55
std::map< MacAddress, std::map< BlockKey, std::vector< std::uint8_t > > > ResponseMap
Result of ReadResponse(): MAC address -> parsed (option,suboption) -> payload blocks.
Definition dcp.h:483
ResponseMap ReadResponse(IRawEthernetSocket &sock, const MacAddress &myMac, double timeoutSec=DEFAULT_RESPONSE_TIMEOUT, bool once=false, std::optional< std::uint32_t > expectedXid=std::nullopt)
Read and parse DCP Identify responses.
Definition dcp.cpp:1218
TEST_F(AlarmListenerTests, InitializesWithEndpoint)
std::array< std::uint8_t, macAddressLength > MacAddress
A 6-byte Ethernet MAC address.
Definition util.h:48
MacAddress String2Mac(const std::string &macStr)
Parse a colon-separated MAC address string.
Definition util.cpp:122
constexpr std::uint16_t PROFINET_ETHERTYPE
EtherType value identifying PROFINET frames (0x8892).
Definition util.h:30
std::vector< std::uint8_t > Bytes
Generic byte buffer alias used throughout the library for raw wire data.
Definition protocol.h:55
std::string Mac2String(const MacAddress &mac)
Format a MAC address as a colon-separated string.
Definition util.cpp:188
PROFINET protocol wire-format structures and serialization helpers.
Plain (untagged) Ethernet II frame header.
Definition protocol.h:479
MacAddress src
Source MAC address.
Definition protocol.h:484
std::uint16_t type
EtherType field.
Definition protocol.h:487
static EthernetHeader Parse(const Bytes &data)
Parse an Ethernet header and payload from raw frame bytes.
Definition protocol.h:495
MacAddress dst
Destination MAC address.
Definition protocol.h:481
Bytes payload
Frame payload following the header.
Definition protocol.h:490
DCP request block: Option(1) + SubOption(1) + Length(2) + payload(Length).
Definition protocol.h:666
std::uint16_t length
Length in bytes of Payload.
Definition protocol.h:675
std::uint8_t suboption
DCP suboption code.
Definition protocol.h:672
Bytes payload
Block payload.
Definition protocol.h:678
DcpOption option
DCP option code.
Definition protocol.h:668
Bytes ToBytes() const
Serialize this block back to raw bytes.
Definition protocol.h:704
DCP (Discovery and Configuration Protocol) frame header.
Definition protocol.h:584
std::uint32_t xId
Transaction ID correlating requests and responses.
Definition protocol.h:600
Bytes ToBytes() const
Serialize this header and its block-list payload back to raw bytes.
Definition protocol.h:637
std::uint16_t respDelay
Response delay factor (requests) or reserved (responses).
Definition protocol.h:603
static PNDCPHeader Parse(const Bytes &data)
Parse a DCP header and block-list payload from raw bytes.
Definition protocol.h:617
DcpFrameId frameId
DCP frame ID identifying the service/multicast group.
Definition protocol.h:591
std::uint16_t length
Length in bytes of the block list that follows.
Definition protocol.h:606
DcpServiceId serviceId
DCP service ID (GET/SET/IDENTIFY/HELLO).
Definition protocol.h:594
DcpServiceType serviceType
DCP service type (REQUEST/RESPONSE/...).
Definition protocol.h:597
Bytes payload
DCP block list payload.
Definition protocol.h:609
Bytes frame
std::unique_ptr< asio::io_context > ioContext
Shared ASIO context.
#define PROFINET_TEST_INTERFACE