12#include <gtest/gtest.h>
30using namespace std::chrono_literals;
31#ifndef PROFINET_TEST_INTERFACE
32#define PROFINET_TEST_INTERFACE ""
36[[nodiscard]] std::string GetIntegrationTestInterface()
44const MacAddress kControllerMac{0x08, 0x00, 0x27, 0x31, 0x39, 0xDC};
46const MacAddress kDeviceMac{0xA0, 0xD3, 0x85, 0x00, 0xF6, 0x13};
59 dcp.
frameId = DcpFrameId::GetSet;
64 dcp.
length =
static_cast<std::uint16_t
>(block.size());
70 frame.reserve(14U + (vlan ? 4U : 0U) + dcpBytes.size());
72 frame.insert(
frame.end(), destination.begin(), destination.end());
74 frame.insert(
frame.end(), source.begin(), source.end());
91 frame.insert(
frame.end(), dcpBytes.begin(), dcpBytes.end());
100Bytes BuildStationNameBlock(
const std::string& stationName)
102 const Bytes nameBytes(stationName.begin(), stationName.end());
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());
112 if ((nameBytes.size() % 2U) != 0U)
114 result.push_back(0x00);
125template <
typename Predicate>
130 for (
int attempt = 0; attempt < 10; ++attempt)
139 if (predicate(
frame))
149class DcpAsioIntegrationTests :
public ::testing::Test
153 void SetUp()
override
155 testInterface = GetIntegrationTestInterface();
157 if (testInterface.empty())
159 GTEST_SKIP() <<
"PROFINET_TEST_INTERFACE is not set. "
160 <<
"Set it before starting VS Code, for example: "
161 <<
"export PROFINET_TEST_INTERFACE=enp0s8";
163 ASSERT_NO_THROW(socket = std::make_unique<AsioEthernetSocket>(
ioContext, testInterface, 0));
165 ASSERT_NO_THROW(peerSocket = std::make_unique<AsioEthernetSocket>(
ioContext, testInterface, 0));
169 void TearDown()
override
182 std::unique_ptr<AsioEthernetSocket> socket;
185 std::unique_ptr<AsioEthernetSocket> peerSocket;
188 std::string testInterface;
195TEST_F(DcpAsioIntegrationTests, SendDiscoverTransmitsIdentifyRequest)
199 const Bytes frame = ReceiveMatchingFrame(*peerSocket, [](
const Bytes& data)
201 if (data.size() < 14)
208 const EthernetHeader ethernet = EthernetHeader::Parse(data);
210 if (ethernet.dst != String2Mac(DCP_MULTICAST_MAC))
215 if (ethernet.src != kControllerMac)
227 return dcp.frameId == DcpFrameId::IdentifyRequest &&
228 dcp.serviceId == DcpServiceId::Identify &&
229 dcp.serviceType == DcpServiceType::Request &&
232 catch (
const std::exception&)
238 ASSERT_FALSE(
frame.empty());
243 EXPECT_EQ(ethernet.
src, kControllerMac);
248 EXPECT_EQ(dcp.
frameId, DcpFrameId::IdentifyRequest);
250 EXPECT_EQ(dcp.
serviceId, DcpServiceId::Identify);
252 EXPECT_EQ(dcp.
serviceType, DcpServiceType::Request);
256 EXPECT_NE(dcp.
xId, 0U);
261TEST_F(DcpAsioIntegrationTests, ReadResponseParsesAsioEthernetFrame)
263 const Bytes block = BuildStationNameBlock(
"AUMA");
265 const std::uint32_t xid = 0x12345678U;
267 const Bytes response = BuildDcpResponseFrame(kControllerMac, kDeviceMac, xid, block);
269 peerSocket->Send(response);
273 ASSERT_EQ(result.size(), 1U);
275 const auto deviceIt = result.find(kDeviceMac);
277 ASSERT_NE(deviceIt, result.end());
279 const auto blockIt = deviceIt->second.find(
MakeBlockKey(DcpOption::Device,
static_cast<std::uint8_t
>(DcpDeviceSuboption::Name)));
281 ASSERT_NE(blockIt, deviceIt->second.end());
283 EXPECT_EQ(blockIt->second, (
Bytes{
'A',
'U',
'M',
'A'}));
288TEST_F(DcpAsioIntegrationTests, ReadResponseParsesVlanTaggedAsioEthernetFrame)
290 const Bytes block = BuildStationNameBlock(
"AUMA");
292 const std::uint32_t xid = 0x23456789U;
294 const Bytes response = BuildDcpResponseFrame(kControllerMac, kDeviceMac, xid, block,
true);
296 peerSocket->Send(response);
300 ASSERT_EQ(result.size(), 1U);
302 const auto deviceIt = result.find(kDeviceMac);
304 ASSERT_NE(deviceIt, result.end());
306 const auto blockIt = deviceIt->second.find(
MakeBlockKey(DcpOption::Device,
static_cast<std::uint8_t
>(DcpDeviceSuboption::Name)));
308 ASSERT_NE(blockIt, deviceIt->second.end());
310 EXPECT_EQ(blockIt->second, (
Bytes{
'A',
'U',
'M',
'A'}));
322TEST_F(DcpAsioIntegrationTests, GetParamPerformsAsioRoundTrip)
324 std::exception_ptr peerException;
325 Bytes receivedRequest;
327 std::jthread peerWorker([&]
331 peerSocket->SetTimeout(1000ms);
333 receivedRequest = peerSocket->Recv();
335 if (receivedRequest.empty())
337 throw std::runtime_error(
"DCP GET request was not received");
342 EXPECT_EQ(requestEthernet.dst, kDeviceMac);
344 EXPECT_EQ(requestEthernet.src, kControllerMac);
350 EXPECT_EQ(requestDcp.
frameId, DcpFrameId::GetSet);
352 EXPECT_EQ(requestDcp.
serviceId, DcpServiceId::Get);
354 EXPECT_EQ(requestDcp.
serviceType, DcpServiceType::Request);
356 EXPECT_EQ(requestDcp.
length, 2U);
358 EXPECT_EQ(requestDcp.
payload.size(), 4U);
359 EXPECT_EQ(requestDcp.
payload[0],
static_cast<std::uint8_t
>(DcpOption::Device));
361 EXPECT_EQ(requestDcp.
payload[1],
static_cast<std::uint8_t
>(DcpDeviceSuboption::Name));
363 EXPECT_EQ(requestDcp.
payload[2], 0x00);
364 EXPECT_EQ(requestDcp.
payload[3], 0x00);
366 const Bytes responseBlock = BuildStationNameBlock(
"AUMA");
368 const Bytes response = BuildDcpResponseFrame(kControllerMac, kDeviceMac, requestDcp.
xId, responseBlock);
370 peerSocket->Send(response);
374 peerException = std::current_exception();
384 std::rethrow_exception(peerException);
387 ASSERT_FALSE(receivedRequest.empty());
388 ASSERT_TRUE(result.has_value());
390 EXPECT_EQ(*result, (
Bytes{
'A',
'U',
'M',
'A'}));
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.
std::optional< std::vector< std::uint8_t > > GetParam(IRawEthernetSocket &sock, const MacAddress &src, const std::string &target, const std::string ¶m, int timeoutSec=DEFAULT_TIMEOUT)
Read a named parameter from a device.
constexpr DcpBlockKey MakeBlockKey(std::uint16_t option, std::uint8_t suboption)
constexpr std::uint16_t DEFAULT_RESPONSE_DELAY_FACTOR
Default dcp response delay factor.
const std::string DCP_MULTICAST_MAC
DCP Identify/Get/Set multicast destination MAC.
std::map< MacAddress, std::map< BlockKey, std::vector< std::uint8_t > > > ResponseMap
Result of ReadResponse(): MAC address -> parsed (option,suboption) -> payload blocks.
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.
TEST_F(AlarmListenerTests, InitializesWithEndpoint)
std::array< std::uint8_t, macAddressLength > MacAddress
A 6-byte Ethernet MAC address.
MacAddress String2Mac(const std::string &macStr)
Parse a colon-separated MAC address string.
constexpr std::uint16_t PROFINET_ETHERTYPE
EtherType value identifying PROFINET frames (0x8892).
std::vector< std::uint8_t > Bytes
Generic byte buffer alias used throughout the library for raw wire data.
std::string Mac2String(const MacAddress &mac)
Format a MAC address as a colon-separated string.
PROFINET protocol wire-format structures and serialization helpers.
DCP request block: Option(1) + SubOption(1) + Length(2) + payload(Length).
std::uint16_t length
Length in bytes of Payload.
std::uint8_t suboption
DCP suboption code.
Bytes payload
Block payload.
DcpOption option
DCP option code.
Bytes ToBytes() const
Serialize this block back to raw bytes.
std::unique_ptr< asio::io_context > ioContext
Shared ASIO context.
#define PROFINET_TEST_INTERFACE