PROFINET IO Controller Stack 1.0.0
Modern C++ implementation of a PROFINET IO Controller stack
Loading...
Searching...
No Matches
dcp.cpp
Go to the documentation of this file.
1
30
31#include "profinet/dcp.h"
32
33#include <algorithm>
34#include <cstdio>
35#include <cstring>
36#include <magic_enum.hpp>
37#include <map>
38#include <random>
39#include <thread>
40
41#include "profinet/exceptions.h"
42#include "profinet/protocol.h"
43#include "profinet/vendors.h"
44
45namespace profinet::dcp
46{
47
48namespace
49{
50
51std::uint32_t GenerateXid()
52{
53 static thread_local std::mt19937 rng{std::random_device{}()};
54 static constexpr std::size_t maxXid = 0xFFFFFFFFULL;
55 static thread_local std::uniform_int_distribution<std::uint64_t> dist(0, maxXid);
56 return static_cast<std::uint32_t>(dist(rng));
57}
58
61template <typename Fn>
62void ForEachBlock(const std::vector<std::uint8_t>& data, Fn&& fn)
63{
64 wire::Reader r(data);
65
66 while (r.Remaining() >= 4U)
67 {
68 const auto option = static_cast<DcpOption>(r.U8());
69 const auto suboption = r.U8();
70 const auto length = r.U16();
71
72 const std::size_t payloadLength =
73 std::min<std::size_t>(length, r.Remaining());
74
75 const Bytes payload = r.ReadBytes(payloadLength);
76
78
79 if (payloadLength < length)
80 {
81 break;
82 }
83
84 if ((length % 2U) != 0U)
85 {
86 if (r.Remaining() < sizeof(std::uint8_t))
87 {
88 break;
89 }
90
91 r.Skip(sizeof(std::uint8_t));
92 }
93 }
94}
95} // namespace
96
97std::vector<std::string> DecodeDeviceRole(std::uint8_t roleByte)
98{
99 std::vector<std::string> roles;
100
101 for (const auto role : magic_enum::enum_values<DcpDeviceRole>())
102 {
103 const auto mask = static_cast<std::uint8_t>(role);
104
105 if ((roleByte & mask) != 0U)
106 {
107 roles.emplace_back(magic_enum::enum_name(role));
108 }
109 }
110
111 if (roles.empty())
112 {
113 roles.emplace_back("Unknown");
114 }
115
116 return roles;
117}
119{
120 return magic_enum::enum_contains(role)
121 ? std::string(magic_enum::enum_name(role))
122 : "Unknown";
123}
124
125std::string GetSuboptionName(std::uint16_t option, std::uint8_t value)
126{
127 const auto dcpOption = static_cast<DcpOption>(option);
128 if (magic_enum::enum_contains(dcpOption))
129 {
130 switch (dcpOption)
131 {
132 case DcpOption::Ip:
133 {
134 const auto suboption = static_cast<DcpIpSuboption>(value);
135
136 if (magic_enum::enum_contains(suboption))
137 {
138 return std::string{magic_enum::enum_name(suboption)};
139 }
140 break;
141 }
142
144 {
145 const auto suboption = static_cast<DcpDeviceSuboption>(value);
146
147 if (magic_enum::enum_contains(suboption))
148 {
149 return std::string{magic_enum::enum_name(suboption)};
150 }
151 break;
152 }
153
154 case DcpOption::Dhcp:
155 {
156 const auto suboption = static_cast<DcpDhcpSuboption>(value);
157
158 if (magic_enum::enum_contains(suboption))
159 {
160 return std::string{magic_enum::enum_name(suboption)};
161 }
162 break;
163 }
164
166 {
167 const auto suboption = static_cast<DcpControlSuboption>(value);
168
169 if (magic_enum::enum_contains(suboption))
170 {
171 return std::string{magic_enum::enum_name(suboption)};
172 }
173 break;
174 }
175
177 {
178 const auto suboption = static_cast<DcpDeviceInitiativeSuboption>(value);
179
180 if (magic_enum::enum_contains(suboption))
181 {
182 return std::string{magic_enum::enum_name(suboption)};
183 }
184 break;
185 }
188 case DcpOption::Nme:
189 case DcpOption::All:
190 break;
191 }
192 }
193 return Hex2(value);
194}
195
196std::string GetBlockName(std::uint16_t option, std::uint8_t suboption)
197{
198 std::string optionName;
199 const auto dcpOption = static_cast<DcpOption>(option);
200
201 if (magic_enum::enum_contains(dcpOption) && dcpOption != DcpOption::Unknown)
202 {
203 optionName = std::string{magic_enum::enum_name(dcpOption)};
204 }
205 else
206 {
208 {
209 optionName = "Vendor-" + Hex2(option);
210 }
211 else
212 {
213 optionName = "Opt-" + Hex2(option);
214 }
215 }
216
217 const auto suboptionName = GetSuboptionName(option, suboption);
218
219 return optionName + "/" + suboptionName;
220}
221
222std::string GetBlockName(DcpOption option, std::uint8_t suboption)
223{
224 return GetBlockName(static_cast<std::uint16_t>(option), suboption);
225}
226
228{
229 return GetBlockName(option, static_cast<std::uint8_t>(suboption));
230}
231
233{
234 return GetBlockName(option, static_cast<std::uint8_t>(suboption));
235}
236
238{
239 return GetBlockName(option, static_cast<std::uint8_t>(suboption));
240}
241
243{
244 return GetBlockName(option, static_cast<std::uint8_t>(suboption));
245}
246
248{
249 return GetBlockName(option, static_cast<std::uint8_t>(suboption));
250}
251
253DcpBlockError RecvSetResponse(IRawEthernetSocket& sock, const MacAddress& srcMac, int timeoutSec)
254{
255 sock.SetTimeout(std::chrono::milliseconds(2000));
256 const MaxTimeout timer{std::chrono::duration<double>(timeoutSec)};
257 while (!timer.TimedOut())
258 {
259 const Bytes data = sock.Recv();
260 if (data.empty() || data.size() < 14)
261 {
262 continue;
263 }
264
265 EthernetHeader eth;
266 try
267 {
268 eth = EthernetHeader::Parse(data);
269 }
270 catch (const std::exception&)
271 {
272 continue;
273 }
274
275 if (eth.dst != srcMac)
276 {
277 continue;
278 }
279
280 Bytes payload = eth.payload;
281
282 if (eth.type == VLAN_ETHERTYPE)
283 {
284 if (payload.size() < 2 * sizeof(std::uint16_t))
285 {
286 continue;
287 }
289 r.Skip(sizeof(std::uint16_t));
290 const auto innerType = r.U16();
291 if (innerType != PROFINET_ETHERTYPE)
292 {
293 continue;
294 }
295 }
296 else if (eth.type != PROFINET_ETHERTYPE)
297 {
298 continue;
299 }
300
301 try
302 {
303 return ParseSetResponse(data);
304 }
305 catch (const DCPError& e)
306 {
307 if (std::string(e.what()).find("unexpected service_type") != std::string::npos)
308 {
309 continue;
310 }
311 throw;
312 }
313 }
314 throw DCPTimeoutError("No DCP SET response received");
315}
316
320{
321 if (data.size() < 14)
322 {
323 throw DCPError("DCP SET response too short");
324 }
325
326 EthernetHeader eth;
327 try
328 {
329 eth = EthernetHeader::Parse(data);
330 }
331 catch (const std::exception& e)
332 {
333 throw DCPError(std::string("Failed to parse Ethernet header: ") + e.what());
334 }
335
336 Bytes payload = eth.payload;
337 if (eth.type == VLAN_ETHERTYPE)
338 {
339 if (payload.size() < 2 * sizeof(std::uint16_t))
340 {
341 throw DCPError("VLAN frame too short");
342 }
344 r.Skip(sizeof(std::uint16_t));
345 const auto innerType = r.U16();
346
347 if (innerType != PROFINET_ETHERTYPE)
348 {
349 throw DCPError("Unexpected inner EtherType: " + Hex4(innerType));
350 }
351 payload = Bytes(payload.begin() + 4, payload.end());
352 }
353 else if (eth.type != PROFINET_ETHERTYPE)
354 {
355 throw DCPError("Unexpected EtherType: " + Hex4(eth.type));
356 }
357
358 PNDCPHeader hdr;
359 try
360 {
362 }
363 catch (const std::exception& e)
364 {
365 throw DCPError(std::string("Failed to parse DCP header: ") + e.what());
366 }
367
369 {
370 throw DCPError("DCP SET: service not supported by device");
371 }
373 {
374 throw DCPError("DCP SET: unexpected service_type " + Hex2(static_cast<std::uint8_t>(hdr.serviceType)));
375 }
376
378 bool found = false;
379 ForEachBlock(hdr.payload, [&](DcpOption option, std::uint8_t suboption, const Bytes& blockPayload)
380 {
381 if (found)
382 {
383 return;
384 }
386 {
387 found = true;
388 if (blockPayload.size() >= 3)
389 {
390 result = static_cast<DcpBlockError>(blockPayload[2]);
391 }
392 else if (!blockPayload.empty())
393 {
394 result = static_cast<DcpBlockError>(blockPayload[0]);
395 }
396 }
397 });
398 // No Control/Response block found - some devices omit it; treat as success.
399 return result;
400}
401
403{
404 switch (info)
405 {
406 case DcpIpBlockInfo::NotSet:
407 return "IP not set";
408 case DcpIpBlockInfo::Set:
409 return "IP set";
410 case DcpIpBlockInfo::SetByDhcp:
411 return "IP set by DHCP";
412 case DcpIpBlockInfo::NotSetConflict:
413 return "IP not set (address conflict detected)";
414 case DcpIpBlockInfo::SetConflict:
415 return "IP set (address conflict detected)";
416 case DcpIpBlockInfo::SetByDhcpConflict:
417 return "IP set by DHCP (address conflict detected)";
418
419 default:
420 return "Unknown (" + Hex4(static_cast<std::uint16_t>(info)) + ")";
421 }
422}
423
425{
426 return (static_cast<std::uint16_t>(info) & DCP_IP_CONFLICT_BIT) != 0U;
427}
428
430{
431 return (static_cast<std::uint16_t>(info) & DCP_IP_DHCP_BIT) != 0U;
432}
433
435{
436 const auto name = magic_enum::enum_name(qualifier);
437
438 if (!name.empty())
439 {
440 return std::string{name};
441 }
442
443 return "Unknown (" +
444 Hex4(static_cast<std::uint16_t>(qualifier)) + ")";
445}
446
447std::string GetResetQualifierName(std::uint16_t qualifier)
448{
449 auto resetQualifier = magic_enum::enum_cast<DcpResetQualifier>(qualifier);
450 if (resetQualifier.has_value())
451 {
452 return GetResetQualifierName(resetQualifier.value());
453 }
454 return "Unknown (" + Hex4(qualifier) + ")";
455}
456
458{
459 switch (qualifier)
460 {
461 case DcpResetQualifier::ApplicationData:
462 case DcpResetQualifier::ApplicationDataAlternate:
463 return "Reset application data";
464 case DcpResetQualifier::CommunicationParameter:
465 case DcpResetQualifier::CommunicationParameterAlternate:
466 return "Reset communication parameter";
467 case DcpResetQualifier::EngineeringParameter:
468 case DcpResetQualifier::EngineeringParameterAlternate:
469 return "Reset engineering parameter";
470 case DcpResetQualifier::AllStoredData:
471 case DcpResetQualifier::AllStoredDataAlternate:
472 return "Reset all stored data";
473
474 case DcpResetQualifier::EngineeringParameterExtended:
475 case DcpResetQualifier::EngineeringParameterExtendedAlternate:
476 return "Reset engineering parameter";
477 case DcpResetQualifier::ToFactory:
478 case DcpResetQualifier::ToFactoryAlternate:
479 return "Reset to factory values";
480 case DcpResetQualifier::Restore:
481 case DcpResetQualifier::RestoreAlternate:
482 return "Reset and restore data";
483 default:
484 return "Unknown (" + Hex4(static_cast<std::uint16_t>(qualifier)) + ")";
485 }
486}
487
489{
490 switch (value)
491 {
492 case DcpDeviceInitiative::NoHello:
493 return "Device does not issue DCP-Hello after power on";
494 case DcpDeviceInitiative::IssueHello:
495 return "Device issues DCP-Hello after power on";
496 default:
497 return "Unknown (" + Hex4(static_cast<std::uint16_t>(value)) + ")";
498 }
499}
500
502{
503 switch (value)
504 {
505 case DcpResponseCode::NoError:
506 return "No error";
507 case DcpResponseCode::OptionNotSupported:
508 return "Option not supported";
509 case DcpResponseCode::SuboptionNotSupported:
510 return "Suboption not supported or no DataSet available";
511 case DcpResponseCode::SuboptionNotSet:
512 return "Suboption not set";
513 case DcpResponseCode::ResourceError:
514 return "Resource error";
515 case DcpResponseCode::SetNotPossible:
516 return "Set not possible";
517 case DcpResponseCode::InOperationSetNotPossible:
518 return "In operation, SET not possible";
519 default:
520 return "Unknown (" + Hex2(static_cast<std::uint8_t>(value)) + ")";
521 }
522}
523
525{
526 switch (error)
527 {
528 case DcpBlockError::Ok:
529 return "Ok";
530
531 case DcpBlockError::OptionUnsupported:
532 return "Option not supported";
533
534 case DcpBlockError::SuboptionUnsupported:
535 return "Suboption not supported or no dataset available";
536
537 case DcpBlockError::SuboptionNotSet:
538 return "Suboption not set";
539
540 case DcpBlockError::Resource:
541 return "Resource error";
542
543 case DcpBlockError::SetNotPossible:
544 return "SET not possible by local reasons";
545
546 case DcpBlockError::InOperation:
547 return "In operation, SET not possible";
548 }
549
550 return "Unknown error (" + Hex2(static_cast<std::uint8_t>(error)) + ")";
551}
552
553DCPDHCPBlock DCPDHCPBlock::Parse(DcpDhcpSuboption suboption, const std::vector<std::uint8_t>& data)
554{
555 DCPDHCPBlock block;
556 block.suboption = suboption;
557 block.suboptionName = suboption != DcpDhcpSuboption::Unknown && magic_enum::enum_contains(suboption)
558 ? std::string{magic_enum::enum_name(suboption)}
559 : Hex2(static_cast<std::uint8_t>(suboption));
560
561 block.rawData = data;
562
563 if (suboption == DcpDhcpSuboption::Hostname)
564 {
565 block.hostname = DecodeBytes(data);
566 }
567 else if (suboption == DcpDhcpSuboption::ClientId)
568 {
569 block.clientId = data;
570 }
571 else if (suboption == DcpDhcpSuboption::VendorSpec)
572 {
573 block.vendorSpecific = data;
574 }
575 else if (suboption == DcpDhcpSuboption::Fqdn)
576 {
577 block.fqdn = DecodeBytes(data);
578 }
579 else if (suboption == DcpDhcpSuboption::Uuid && data.size() >= uuidLenght)
580 {
581 block.uuid = ToHex(&data[0], 4) + "-" +
582 ToHex(&data[4], 2) + "-" +
583 ToHex(&data[6], 2) + "-" +
584 ToHex(&data[8], 2) + "-" +
585 ToHex(&data[10], 6);
586 }
587 return block;
588}
589
590const std::map<std::string, BlockKey>& Params()
591{
592 static const std::map<std::string, BlockKey> table = {
593 {"name", MakeBlockKey(static_cast<DcpOption>(PNDCPBlock::NAME_OF_STATION.first), PNDCPBlock::NAME_OF_STATION.second)},
594 {"ip", MakeBlockKey(static_cast<DcpOption>(PNDCPBlock::IP_ADDRESS.first), PNDCPBlock::IP_ADDRESS.second)},
595 };
596 return table;
597}
598
599// =============================================================================
600// DCPDeviceDescription
601// =============================================================================
602
603DCPDeviceDescription::DCPDeviceDescription(const MacAddress& macBytes,
604 const std::map<BlockKey, std::vector<std::uint8_t>>& blocks)
605{
606 mac = Mac2String(macBytes);
607
608 auto get = [&](BlockKey key) -> const std::vector<std::uint8_t>*
609 {
610 auto it = blocks.find(key);
611 return it != blocks.end() ? &it->second : nullptr;
612 };
613
614 if (const auto* typeBlock = get(MakeBlockKey(static_cast<DcpOption>(PNDCPBlock::DEVICE_TYPE.first), PNDCPBlock::DEVICE_TYPE.second)))
615 {
616 auto firstNonNull = std::find_if(typeBlock->begin(), typeBlock->end(), [](char c)
617 {
618 return c != '\0';
619 });
620 const std::string deviceTypeString(firstNonNull, typeBlock->end());
621 this->deviceType = deviceTypeString;
622 }
623 if (const auto* nameBlock = get(MakeBlockKey(static_cast<DcpOption>(PNDCPBlock::NAME_OF_STATION.first), PNDCPBlock::NAME_OF_STATION.second)))
624 {
625 auto firstNonNull = std::find_if(nameBlock->begin(), nameBlock->end(), [](char c)
626 {
627 return c != '\0';
628 });
629
630 const std::string stationName(firstNonNull, nameBlock->end());
631 this->name = stationName;
632 }
633
634 if (const auto* ipBlock = get(MakeBlockKey(static_cast<DcpOption>(PNDCPBlock::IP_ADDRESS.first), PNDCPBlock::IP_ADDRESS.second)); (ipBlock != nullptr) && ipBlock->size() >= 12)
635 {
636 if (ipBlock->size() >= 14U) // ipBlockInfo (2) ip (4), netmask (4), gw (4)
637 {
638 wire::Reader r(*ipBlock);
639
640 ipBlockInfo = static_cast<DcpIpBlockInfo>(r.U16());
641 ipConflict = HasConflict(ipBlockInfo);
642 ipSetByDhcp = IsDhcp(ipBlockInfo);
643
644 ip = String2Ip(r.Fixed<4U>());
645 netmask = String2Ip(r.Fixed<4U>());
646 gateway = String2Ip(r.Fixed<4U>());
647 }
648 else if (ipBlock->size() >= 12U) // ip (4), netmask (4), gw (4)
649 {
650 wire::Reader r(*ipBlock);
651
652 ip = String2Ip(r.Fixed<4U>());
653 netmask = String2Ip(r.Fixed<4U>());
654 gateway = String2Ip(r.Fixed<4U>());
655 }
656 }
657
658 if (const auto* devId = get(MakeBlockKey(static_cast<DcpOption>(PNDCPBlock::DEVICE_ID.first), PNDCPBlock::DEVICE_ID.second)); devId && devId->size() >= 4)
659 {
660 wire::Reader r(*devId);
661 r.Skip(devId->size() - 4U);
662
663 vendorId = r.U16();
664 deviceId = r.U16();
665
666 vendorHigh = ByteAt(vendorId, 1U);
667 vendorLow = ByteAt(vendorId, 0U);
668 deviceHigh = ByteAt(deviceId, 1U);
669 deviceLow = ByteAt(deviceId, 0U);
670 }
671
672 if (const auto* roleBlock = get(MakeBlockKey(static_cast<DcpOption>(PNDCPBlock::DEVICE_ROLE.first), PNDCPBlock::DEVICE_ROLE.second)); roleBlock && !roleBlock->empty())
673 {
674 wire::Reader r(*roleBlock);
675
676 this->deviceRole = r.U8();
677 this->deviceRoles = DecodeDeviceRole(this->deviceRole);
678 }
679
680 if (const auto* inst = get(MakeBlockKey(DcpOption::Device, DcpDeviceSuboption::Instance)); inst && inst->size() >= 2)
681 {
682 wire::Reader r(*inst);
683 deviceInstance = {r.U8(), r.U8()};
684 }
685
686 if (const auto* alias = get(MakeBlockKey(DcpOption::Device, DcpDeviceSuboption::Alias)))
687 {
688 aliasName = DecodeBytes(*alias);
689 }
690
691 if (const auto* opts = get(MakeBlockKey(static_cast<DcpOption>(PNDCPBlock::DEVICE_OPTIONS.first), PNDCPBlock::DEVICE_OPTIONS.second)); opts && opts->size() >= 2)
692 {
693 wire::Reader r(*opts);
694
695 for (std::size_t i = 0; i + 1 < opts->size(); i += sizeof(std::uint16_t))
696 {
697 const auto option = static_cast<DcpOption>(r.U8());
698 const auto suboption = r.U8();
699
700 supportedOptions.emplace_back(option, suboption);
701 }
702 }
703
704 for (const auto& [key, Data] : blocks)
705 {
706 if (key.option == DcpOption::Dhcp)
707 {
708 dhcpBlocks.push_back(DCPDHCPBlock::Parse(static_cast<DcpDhcpSuboption>(key.suboption), Data));
709 }
710 }
711
712 if (const auto* init = get(MakeBlockKey(DcpOption::DeviceInitiative, DcpDeviceInitiativeSuboption::Initiative));
713 (init != nullptr) && init->size() >= 2)
714 {
715 wire::Reader r(*init);
716 deviceInitiative = static_cast<DcpDeviceInitiative>(r.U16());
717 issuesHello = deviceInitiative == DcpDeviceInitiative::IssueHello;
718 }
719
720 static const std::vector<BlockKey> known = {
721 MakeBlockKey(static_cast<DcpOption>(PNDCPBlock::IP_ADDRESS.first), PNDCPBlock::IP_ADDRESS.second),
722 MakeBlockKey(static_cast<DcpOption>(PNDCPBlock::DEVICE_TYPE.first), PNDCPBlock::DEVICE_TYPE.second),
723 MakeBlockKey(static_cast<DcpOption>(PNDCPBlock::NAME_OF_STATION.first), PNDCPBlock::NAME_OF_STATION.second),
724 MakeBlockKey(static_cast<DcpOption>(PNDCPBlock::DEVICE_ID.first), PNDCPBlock::DEVICE_ID.second),
725 MakeBlockKey(static_cast<DcpOption>(PNDCPBlock::DEVICE_ROLE.first), PNDCPBlock::DEVICE_ROLE.second),
726 MakeBlockKey(static_cast<DcpOption>(PNDCPBlock::DEVICE_OPTIONS.first), PNDCPBlock::DEVICE_OPTIONS.second),
727 MakeBlockKey(static_cast<DcpOption>(PNDCPBlock::DEVICE_ALIAS.first), PNDCPBlock::DEVICE_ALIAS.second),
728 MakeBlockKey(static_cast<DcpOption>(PNDCPBlock::DEVICE_INSTANCE.first), PNDCPBlock::DEVICE_INSTANCE.second),
729 MakeBlockKey(DcpOption::Device, DcpDeviceSuboption::Instance),
730 MakeBlockKey(DcpOption::Device, DcpDeviceSuboption::Alias),
731 MakeBlockKey(DcpOption::DeviceInitiative, DcpDeviceInitiativeSuboption::Initiative),
732 };
733 for (const auto& [key, value] : blocks)
734 {
735 if (key.option == DcpOption::Dhcp)
736 {
737 continue;
738 }
739 if (std::ranges::find(known, key) == known.end())
740 {
741 rawBlocks[key] = value;
742 }
743 }
744}
745
746std::string DCPDeviceDescription::VendorName() const
747{
748 return GetVendorName(VendorId());
749}
750
751std::string DCPDeviceDescription::ToString() const
752{
753 std::string out = "PROFINET Device: " + name + "\n";
754 out += " MAC: " + mac + "\n";
755 if (!deviceType.empty())
756 {
757 out += " Type: " + deviceType + "\n";
758 }
759 out += " IP: " + ip + "\n";
760 out += " Netmask: " + netmask + "\n";
761 out += " Gateway: " + gateway + "\n";
762 if (ipBlockInfo != DcpIpBlockInfo::NotSet)
763 {
764 out += " IP Info: " + GetIPBlockInfoName(ipBlockInfo) + "\n";
765 }
766 if (ipConflict)
767 {
768 out += " Warning: IP address conflict detected\n";
769 }
770 if (ipSetByDhcp)
771 {
772 out += " IP Source: DHCP\n";
773 }
774 out += " Vendor: " + VendorName() + " (" + Hex4(VendorId()) + ")\n";
775 out += " Device: " + Hex4(DeviceId()) + "\n";
776 if (!deviceRoles.empty())
777 {
778 out += " Role: ";
779 for (std::size_t i = 0; i < deviceRoles.size(); ++i)
780 {
781 out += deviceRoles[i];
782 if (i + 1 < deviceRoles.size())
783 {
784 out += ", ";
785 }
786 }
787 out += "\n";
788 }
789 if (deviceInstance != std::pair<std::uint8_t, std::uint8_t>{0, 0})
790 {
791 out += " Instance: " + std::to_string(deviceInstance.first) + "." +
792 std::to_string(deviceInstance.second) + "\n";
793 }
794 if (!aliasName.empty())
795 {
796 out += " Alias: " + aliasName + "\n";
797 }
798 if (deviceInitiative != DcpDeviceInitiative::NoHello)
799 {
800 out += " Initiative: " + GetDeviceInitiativeName(deviceInitiative) + "\n";
801 }
802 if (!supportedOptions.empty())
803 {
804 out += " Supports: ";
805 for (std::size_t i = 0; i < supportedOptions.size(); ++i)
806 {
807 out += GetBlockName(static_cast<DcpOption>(supportedOptions[i].option), supportedOptions[i].suboption);
808 if (i + 1 < supportedOptions.size())
809 {
810 out += ", ";
811 }
812 }
813 out += "\n";
814 }
815 if (!dhcpBlocks.empty())
816 {
817 out += " DHCP:\n";
818 for (const auto& b : dhcpBlocks)
819 {
820 if (b.hostname)
821 {
822 out += " Hostname: " + *b.hostname + "\n";
823 }
824 else if (b.fqdn)
825 {
826 out += " FQDN: " + *b.fqdn + "\n";
827 }
828 else if (b.uuid)
829 {
830 out += " UUID: " + *b.uuid + "\n";
831 }
832 else
833 {
834 out += " " + b.suboptionName + ": " + ToHex(b.rawData) + "\n";
835 }
836 }
837 }
838 for (const auto& [key, Data] : rawBlocks)
839 {
840 out += " Unknown (" +
841 std::to_string(static_cast<int>(key.option)) + "," +
842 std::to_string(static_cast<int>(key.suboption)) +
843 "): " + ToHex(Data) + "\n";
844 }
845 if (!out.empty() && out.back() == '\n')
846 {
847 out.pop_back();
848 }
849 return out;
850}
851
852// =============================================================================
853// Frame builders / parsers (internal helpers)
854// =============================================================================
855
856namespace
857{
858
859Bytes BuildEthernetDcp(const MacAddress& dst, const MacAddress& src, const PNDCPHeader& dcp)
860{
861 EthernetHeader eth;
862 eth.dst = dst;
863 eth.src = src;
865 eth.payload = dcp.ToBytes();
866 return eth.ToBytes();
867}
868
869bool EqualsIcase(std::string_view lhs, std::string_view rhs)
870{
871 return std::ranges::equal(lhs, rhs, [](char a, char b)
872 {
873 return std::tolower(static_cast<unsigned char>(a)) ==
874 std::tolower(static_cast<unsigned char>(b));
875 });
876}
877
878} // namespace
879
880// =============================================================================
881// Public operations
882// =============================================================================
884 const MacAddress& src,
885 const std::string& param,
886 const std::string& value,
887 bool permanent)
888{
889 if (EqualsIcase(param, "ip"))
890 {
891 // The IP suite must be sent as 12 binary bytes(IP + mask + gateway),
892 // not an ASCII string; an ASCII value fails the device's length
893 // check and is never applied.
894 throw DCPError("Use SetIp() to configure the IP address");
895 }
896 auto it = Params().find(param);
897 if (it == Params().end())
898 {
899 throw DCPError("Unknown parameter: '" + param + "'");
900 }
901 if (param == "name" && value.size() > DCP_MAX_NAME_LENGTH)
902 {
903 throw ValidationError("Station name exceeds maximum length: " + std::to_string(value.size()) +
904 " > " + std::to_string(DCP_MAX_NAME_LENGTH));
905 }
906 const BlockKey key = it->second;
907 Bytes valueBytes = ToVec(value);
908 const std::uint32_t xid = GenerateXid();
909 const std::uint16_t qualifier = permanent ? 0x0001 : 0x0000;
910
911 PNDCPBlockRequest block;
912 block.option = key.option;
913 block.suboption = key.suboption;
914 block.length = static_cast<std::uint16_t>(valueBytes.size() + 2);
915 wire::PutBigEndian(block.payload, qualifier);
916 wire::PutFixed(block.payload, valueBytes);
917
918 // const std::uint16_t padding = valueBytes.size() % 2 == 1 ? 1 : 0;
919
920 auto blockData = block.ToBytes();
921 if (valueBytes.size() % 2 == 1)
922 {
923 blockData.push_back(0x00);
924 }
925
926 PNDCPHeader dcp;
927 dcp.frameId = DcpFrameId::GetSet;
928 dcp.serviceId = DcpServiceId::Set;
929 dcp.serviceType = DcpServiceType::Request;
930 dcp.xId = xid;
931 dcp.respDelay = 0;
932 dcp.length = static_cast<std::uint16_t>(blockData.size());
933 dcp.payload = blockData;
934 return BuildEthernetDcp(dst, src, dcp);
935}
936
938{
939 const std::uint32_t xid = GenerateXid();
940
941 /*
942 const int durationUnits = std::max(1, durationMs / 100);
943 const Bytes blockData = {0x00, 0x01, static_cast<std::uint8_t>((durationUnits >> OneOctetShift) & LowByteMask),
944 static_cast<std::uint8_t>(durationUnits & LowByteMask)};
945*/
946 // Signal block data: BlockQualifier 0x0000 + SignalValue 0x0100
947 // ("flash once") - the only value defined by IEC 61158-6-10
948 const Bytes blockData = {0x00, 0x00, 0x01, 0x00};
949 PNDCPBlockRequest block;
950 block.option = DcpOption::Control;
951 block.suboption = static_cast<std::uint8_t>(DcpControlSuboption::Signal);
952 block.length = static_cast<std::uint16_t>(blockData.size());
953 block.payload = blockData;
954
955 PNDCPHeader dcp;
956 dcp.frameId = DcpFrameId::GetSet;
957 dcp.serviceId = DcpServiceId::Set;
958 dcp.serviceType = DcpServiceType::Request;
959 dcp.xId = xid;
960 dcp.respDelay = 0;
961 dcp.length = static_cast<std::uint16_t>(blockData.size() + 4);
962 dcp.payload = block.ToBytes();
963 return BuildEthernetDcp(
964 dst,
965 src,
966 dcp);
967}
968/*
969Bytes BuildSignalRequest(
970 const MacAddress& dst,
971 const MacAddress& src)
972{
973 constexpr Bytes signalBlockData{
974 0x00,
975 0x00,
976 0x01,
977 0x00};
978
979 const std::uint32_t xid = GenerateXid();
980
981 PNDCPBlockRequest block;
982 block.option = DcpOption::Control;//DCP_OPTION_CONTROL;
983 block.suboption = DcpControlSuboption::Signal;//DCP_SUBOPTION_CONTROL_SIGNAL;
984 block.length =
985 static_cast<std::uint16_t>(
986 signalBlockData.size());
987 block.payload = signalBlockData;
988
989 PNDCPHeader dcp;
990 dcp.frameId = DcpFrameId::GetSet;//DCP_GET_SET_FRAME_ID;
991 dcp.serviceId = DcpServiceId::Set;//PNDCPHeader::SET;
992 dcp.serviceType = DcpServiceType::Request;// PNDCPHeader::REQUEST;
993 dcp.xId = xid;
994 dcp.respDelay = 0;
995 dcp.length =
996 static_cast<std::uint16_t>(
997 signalBlockData.size() + 4U);
998 dcp.payload = block.ToBytes();
999
1000 return BuildEthernetDcp(
1001 dst,
1002 src,
1003 dcp);
1004}*/
1005std::optional<Bytes> GetParam(IRawEthernetSocket& sock, const MacAddress& src, const std::string& target,
1006 const std::string& param, int timeoutSec)
1007{
1008 auto it = Params().find(param);
1009 if (it == Params().end())
1010 {
1011 throw DCPError("Unknown parameter: '" + param + "'");
1012 }
1013
1014 const MacAddress dst = String2Mac(target);
1015 const BlockKey key = it->second;
1016 const std::uint32_t xid = GenerateXid();
1017
1018 PNDCPBlockRequest block;
1019 block.option = key.option;
1020 block.suboption = key.suboption;
1021 block.length = 0;
1022
1023 PNDCPHeader dcp;
1024 dcp.frameId = DcpFrameId::GetSet;
1025 dcp.serviceId = DcpServiceId::Get;
1026 dcp.serviceType = DcpServiceType::Request;
1027 dcp.xId = xid;
1028 dcp.respDelay = 0;
1029 // A DCP GET request does NOT contain a normal DCP block header
1030
1031 // For a GET request, the data after the DCP header is a selector:
1032
1033 // Option 1 byte
1034 // SubOption 1 byte
1035 // ------------------
1036 // 2 bytes
1037 // DCPDataLength = 2
1038 // Wireshark confirms this directly. In its DCP PDU dissector, after reading DCPDataLength, it specifically handles:
1039 // GET + REQUEST
1040 // -> dissect_PNDCP_Option()
1041 // rather than dissect_PNDCP_Block(). That function reads exactly:
1042 // Option 1 byte
1043 // SubOption 1 byte
1044 // so the GET request consumes 2 bytes.
1045 // https://github.com/boundary/wireshark/blob/master/plugins/profinet/packet-pn-dcp.c
1046
1047 dcp.length = 2; // Length of expected parameter to read not: (sizeof(std::uint8_t) * 2) + sizeof(std::uint16_t); // option + suboption + length
1048
1049 // BUT The IEC 61158-6-10 material I found defines:
1050 // DCPDataLength = total length of the data following the DCP-UC header
1051 // so 4 instead of 2..
1052
1053 //
1054 dcp.payload = block.ToBytes(); // euh Need to check with spec..
1055 // For Get don't use PNDCPBlockRequest, just a 2 byte Get selector, don't add the 2 bytes for length.
1056 // dcp.payload = {
1057 // static_cast<std::uint8_t>(key.option),
1058 // key.suboption};
1059
1060 sock.Send(BuildEthernetDcp(dst, src, dcp));
1061
1062 const auto responses = ReadResponse(sock, src, timeoutSec, /*once=*/true, xid);
1063 std::fprintf(stderr,
1064 "GetParam: xid=%08x responses=%zu\n",
1065 xid,
1066 responses.size());
1067 for (const auto& [mac, blocks] : responses)
1068 {
1069 std::fprintf(stderr,
1070 " response src=%02x:%02x:%02x:%02x:%02x:%02x blocks=%zu\n",
1071 mac[0], mac[1], mac[2],
1072 mac[3], mac[4], mac[5],
1073 blocks.size());
1074
1075 for (const auto& [blockKey, value] : blocks)
1076 {
1077 std::fprintf(stderr,
1078 " block option=%02x suboption=%02x size=%zu\n",
1079 static_cast<unsigned>(blockKey.option),
1080 static_cast<unsigned>(blockKey.suboption),
1081 value.size());
1082 }
1083 }
1084 if (!responses.empty())
1085 {
1086 const auto& blocks = responses.begin()->second;
1087 auto bit = blocks.find(key);
1088 if (bit != blocks.end())
1089 {
1090 return bit->second;
1091 }
1092 }
1093 return std::nullopt;
1094}
1095
1096bool SetParam(IRawEthernetSocket& sock, const MacAddress& src, const std::string& target,
1097 const std::string& param, const std::string& value, int timeoutSec, bool permanent, int threadSleepMs)
1098{
1099 const MacAddress dst = String2Mac(target);
1100
1101 const Bytes frame = BuildSetParamRequest(dst, src, param, value, permanent);
1102 sock.Send(frame);
1103
1104 try
1105 {
1106 const auto blockError = RecvSetResponse(sock, src, timeoutSec);
1107 if (blockError != DcpBlockError::Ok)
1108 {
1109 throw DCPError("DCP SET failed for '" + param + "': " + GetDcpBlockErrorDescription(blockError));
1110 }
1111 std::this_thread::sleep_for(std::chrono::milliseconds(threadSleepMs));
1112 return true;
1113 }
1114 catch (const DCPTimeoutError&)
1115 {
1116 return false;
1117 }
1118}
1119
1120bool SetIp(IRawEthernetSocket& sock, const MacAddress& src, const std::string& target, const std::string& ip,
1121 const std::string& netmask, const std::string& gateway, bool permanent, int timeoutSec, int threadSleepMs)
1122{
1123 const MacAddress dst = String2Mac(target);
1124 const std::uint32_t xid = GenerateXid();
1125
1126 auto ipBytes = Ip2String(ip);
1127 auto netmaskBytes = Ip2String(netmask);
1128 auto gatewayBytes = Ip2String(gateway);
1129
1130 Bytes valueBytes;
1131 valueBytes.insert(valueBytes.end(), ipBytes.begin(), ipBytes.end());
1132 valueBytes.insert(valueBytes.end(), netmaskBytes.begin(), netmaskBytes.end());
1133 valueBytes.insert(valueBytes.end(), gatewayBytes.begin(), gatewayBytes.end());
1134
1135 const DcpBlockQualifier qualifier = permanent ? DcpBlockQualifier::Permanent : DcpBlockQualifier::Temporary;
1136
1137 PNDCPBlockRequest block;
1138 block.option = static_cast<DcpOption>(PNDCPBlock::IP_ADDRESS.first);
1139 block.suboption = PNDCPBlock::IP_ADDRESS.second;
1140 block.length = static_cast<std::uint16_t>(valueBytes.size() + 2);
1141 wire::PutBigEndian(block.payload, static_cast<std::uint16_t>(qualifier));
1142 wire::PutFixed(block.payload, valueBytes);
1143
1144 const std::uint16_t padding = valueBytes.size() % 2 == 0 ? 0 : 1;
1145
1146 PNDCPHeader dcp;
1147 dcp.frameId = DcpFrameId::GetSet;
1148 dcp.serviceId = DcpServiceId::Set;
1149 dcp.serviceType = DcpServiceType::Request;
1150 dcp.xId = xid;
1151 dcp.respDelay = 0;
1152 dcp.length = static_cast<std::uint16_t>(valueBytes.size() + macAddressLength + padding);
1153 dcp.payload = block.ToBytes();
1154
1155 sock.Send(BuildEthernetDcp(dst, src, dcp));
1156
1157 try
1158 {
1159 const auto blockError = RecvSetResponse(sock, src, timeoutSec);
1160 if (blockError != DcpBlockError::Ok)
1161 {
1162 throw DCPError("DCP SET IP failed: " + GetDcpBlockErrorDescription(blockError));
1163 }
1164 std::this_thread::sleep_for(std::chrono::milliseconds(threadSleepMs));
1165 return true;
1166 }
1167 catch (const DCPTimeoutError&)
1168 {
1169 return false;
1170 }
1171}
1172
1173void SendDiscover(IRawEthernetSocket& sock, const MacAddress& src, std::uint16_t responseDelay)
1174{
1175 const std::uint32_t xid = GenerateXid();
1176
1177 PNDCPBlockRequest block;
1178 block.option = DcpOption::All;
1179 block.suboption = 0xFF;
1180 block.length = 0;
1181
1182 PNDCPHeader dcp;
1183 dcp.frameId = DcpFrameId::IdentifyRequest;
1184 dcp.serviceId = DcpServiceId::Identify;
1185 dcp.serviceType = DcpServiceType::Request;
1186 dcp.xId = xid;
1187 dcp.respDelay = responseDelay;
1188 const Bytes blockBytes = block.ToBytes();
1189 dcp.length = static_cast<std::uint16_t>(blockBytes.size());
1190 dcp.payload = blockBytes;
1191
1192 sock.Send(BuildEthernetDcp(String2Mac(DCP_MULTICAST_MAC), src, dcp));
1193}
1194
1195void SendRequest(IRawEthernetSocket& sock, const MacAddress& src, BlockKey blockType, const Bytes& value)
1196{
1197 const std::uint32_t xid = GenerateXid();
1198
1199 PNDCPBlockRequest block;
1200 block.option = blockType.option;
1201 block.suboption = blockType.suboption;
1202 block.length = static_cast<std::uint16_t>(value.size());
1203 block.payload = value;
1204
1205 PNDCPHeader dcp;
1206 dcp.frameId = DcpFrameId::IdentifyRequest;
1207 dcp.serviceId = DcpServiceId::Identify;
1208 dcp.serviceType = DcpServiceType::Request;
1209 dcp.xId = xid;
1210 dcp.respDelay = 0x0080;
1211 const Bytes blockBytes = block.ToBytes();
1212 dcp.length = static_cast<std::uint16_t>(blockBytes.size());
1213 dcp.payload = blockBytes;
1214
1215 sock.Send(BuildEthernetDcp(String2Mac(DCP_MULTICAST_MAC), src, dcp));
1216}
1217
1218ResponseMap ReadResponse(IRawEthernetSocket& sock, const MacAddress& myMac, double timeoutSec, bool once,
1219 std::optional<std::uint32_t> expectedXid)
1220{
1221 ResponseMap result;
1222 sock.SetTimeout(std::chrono::milliseconds(2000));
1223 const MaxTimeout timer{std::chrono::duration<double>(timeoutSec)};
1224
1225 while (!timer.TimedOut())
1226 {
1227 const Bytes data = sock.Recv();
1228 std::fprintf(stderr,
1229 "ReadResponse: received %zu bytes, expectedXid=%08x\n",
1230 data.size(),
1231 expectedXid.value_or(0U));
1232 if (data.empty())
1233 {
1234 if (once)
1235 {
1236 break;
1237 }
1238 continue;
1239 }
1240
1241 if (data.size() < 14)
1242 {
1243 continue;
1244 }
1245
1246 EthernetHeader eth;
1247 try
1248 {
1249 eth = EthernetHeader::Parse(data);
1250 std::fprintf(stderr,
1251 "ReadResponse: eth dst=%02x:%02x:%02x:%02x:%02x:%02x "
1252 "src=%02x:%02x:%02x:%02x:%02x:%02x type=%04x\n",
1253 eth.dst[0], eth.dst[1], eth.dst[2],
1254 eth.dst[3], eth.dst[4], eth.dst[5],
1255 eth.src[0], eth.src[1], eth.src[2],
1256 eth.src[3], eth.src[4], eth.src[5],
1257 eth.type);
1258 }
1259 catch (const std::exception&)
1260 {
1261 continue;
1262 }
1263
1264 if (eth.dst != myMac)
1265 {
1266 std::fprintf(stderr, "ReadResponse: rejected: destination mismatch\n");
1267 continue;
1268 }
1269
1270 Bytes payload = eth.payload;
1272 if (eth.type == VLAN_ETHERTYPE)
1273 {
1274 if (payload.size() < 2 * sizeof(std::uint16_t))
1275 {
1276 continue;
1277 }
1278 r.Skip(sizeof(std::uint16_t));
1279 const auto innerType = r.U16();
1280 if (innerType != PROFINET_ETHERTYPE)
1281 {
1282 continue;
1283 }
1284 payload = Bytes(payload.begin() + 4, payload.end());
1285 }
1286 else if (eth.type != PROFINET_ETHERTYPE)
1287 {
1288 continue;
1289 }
1290
1291 PNDCPHeader dcp;
1292 try
1293 {
1294 dcp = PNDCPHeader::Parse(payload);
1295 std::fprintf(stderr,
1296 "ReadResponse: dcp serviceType=%02x xid=%08x length=%u payload=%zu\n",
1297 static_cast<unsigned>(dcp.serviceType),
1298 dcp.xId,
1299 dcp.length,
1300 dcp.payload.size());
1301 }
1302 catch (const std::exception&)
1303 {
1304 continue;
1305 }
1306
1307 if (dcp.serviceType != DcpServiceType::ResponseSuccess)
1308 {
1309 std::fprintf(stderr, "ReadResponse: rejected: not ResponseSuccess\n");
1310 continue;
1311 }
1312 if (expectedXid && dcp.xId != *expectedXid)
1313 {
1314 std::fprintf(stderr,
1315 "ReadResponse: rejected: XID mismatch, expected=%08x actual=%08x\n",
1316 *expectedXid,
1317 dcp.xId);
1318 continue;
1319 }
1320
1321 std::map<BlockKey, Bytes> parsed;
1322 // dcp.length includes the whole block-list; each block ends 2-byte aligned.
1323 const std::size_t consumedLimit = dcp.length > 6 ? static_cast<std::size_t>(dcp.length) : 0;
1324 const Bytes blocksRegion = dcp.payload.size() > consumedLimit
1325 ? Bytes(dcp.payload.begin(), dcp.payload.begin() + consumedLimit)
1326 : dcp.payload;
1327 ForEachBlock(blocksRegion, [&](DcpOption option, std::uint8_t suboption, const Bytes& blockPayload)
1328 {
1329 parsed[MakeBlockKey(option, suboption)] = blockPayload;
1330 });
1331
1332 result[eth.src] = std::move(parsed);
1333 if (once)
1334 {
1335 break;
1336 }
1337 }
1338
1339 return result;
1340}
1341
1342void SendHello(IRawEthernetSocket& sock, const MacAddress& src, const std::string& stationName,
1343 const std::string& ip, const std::string& netmask, const std::string& gateway,
1344 std::pair<std::uint16_t, std::uint16_t> deviceId, DcpDeviceRole deviceRole)
1345{
1346 const std::uint32_t xid = GenerateXid();
1347 Bytes blocksData;
1348
1349 // Name of Station block
1350 const Bytes nameBytes = ToVec(stationName);
1351 PNDCPBlockRequest nameBlock;
1352 nameBlock.option = DcpOption::Device;
1353 nameBlock.suboption = static_cast<std::uint8_t>(DcpDeviceSuboption::Name);
1354 nameBlock.length = static_cast<std::uint16_t>(nameBytes.size());
1355 nameBlock.payload = nameBytes;
1356 Bytes nameBlockBytes = nameBlock.ToBytes();
1357 blocksData.insert(blocksData.end(), nameBlockBytes.begin(), nameBlockBytes.end());
1358 if (nameBytes.size() % 2 == 1)
1359 {
1360 blocksData.push_back(0x00);
1361 }
1362
1363 // IP block
1364 auto ipB = Ip2String(ip);
1365 auto nmB = Ip2String(netmask);
1366 auto gwB = Ip2String(gateway);
1367 Bytes ipPayload;
1368 ipPayload.insert(ipPayload.end(), ipB.begin(), ipB.end());
1369 ipPayload.insert(ipPayload.end(), nmB.begin(), nmB.end());
1370 ipPayload.insert(ipPayload.end(), gwB.begin(), gwB.end());
1371 PNDCPBlockRequest ipBlock;
1372 ipBlock.option = DcpOption::Ip;
1373 ipBlock.suboption = static_cast<std::uint8_t>(DcpIpSuboption::IpParameter);
1374 ipBlock.length = static_cast<std::uint16_t>(ipPayload.size());
1375 ipBlock.payload = ipPayload;
1376
1377 Bytes ipBlockBytes = ipBlock.ToBytes();
1378 wire::PutFixed(blocksData, ipBlockBytes);
1379
1380 // Device ID block
1381 Bytes deviceIdBytes;
1382 deviceIdBytes.reserve(4U);
1383 wire::PutBigEndian(deviceIdBytes, deviceId.first);
1384 wire::PutBigEndian(deviceIdBytes, deviceId.second);
1385
1386 PNDCPBlockRequest deviceIdBlock;
1387 deviceIdBlock.option = DcpOption::Device;
1388 deviceIdBlock.suboption = static_cast<std::uint8_t>(DcpDeviceSuboption::DeviceId);
1389 deviceIdBlock.length = static_cast<std::uint16_t>(deviceIdBytes.size());
1390 deviceIdBlock.payload = deviceIdBytes;
1391 Bytes deviceIdBlockBytes = deviceIdBlock.ToBytes();
1392 wire::PutFixed(blocksData, deviceIdBlockBytes);
1393
1394 // Device Role block
1395 PNDCPBlockRequest roleBlock;
1396 roleBlock.option = DcpOption::Device;
1397 roleBlock.suboption = static_cast<std::uint8_t>(DcpDeviceSuboption::Role);
1398 roleBlock.length = 2;
1399 roleBlock.payload = {static_cast<std::uint8_t>(deviceRole), 0x00};
1400 Bytes roleBlockBytes = roleBlock.ToBytes();
1401 blocksData.insert(blocksData.end(), roleBlockBytes.begin(), roleBlockBytes.end());
1402
1403 // Device Initiative block
1404 PNDCPBlockRequest initBlock;
1405 initBlock.option = DcpOption::DeviceInitiative;
1406 initBlock.suboption = static_cast<std::uint8_t>(DcpDeviceInitiativeSuboption::Initiative);
1407 initBlock.length = 2;
1408 initBlock.payload = {0x00, static_cast<std::uint8_t>(DcpDeviceInitiative::IssueHello)};
1409 Bytes initBlockBytes = initBlock.ToBytes();
1410 blocksData.insert(blocksData.end(), initBlockBytes.begin(), initBlockBytes.end());
1411
1412 PNDCPHeader dcp;
1413 dcp.frameId = DcpFrameId::Hello;
1414 dcp.serviceId = DcpServiceId::Hello;
1415 dcp.serviceType = DcpServiceType::Request;
1416 dcp.xId = xid;
1417 dcp.respDelay = 0;
1418 dcp.length = static_cast<std::uint16_t>(blocksData.size());
1419 dcp.payload = blocksData;
1420
1421 sock.Send(BuildEthernetDcp(String2Mac(DCP_HELLO_MULTICAST_MAC), src, dcp));
1422}
1423
1424std::vector<DCPDeviceDescription> ReceiveHello(IRawEthernetSocket& sock, const MacAddress& /*myMac*/,
1425 double timeoutSec,
1426 const std::function<void(const DCPDeviceDescription&)>& callback)
1427{
1428 std::vector<DCPDeviceDescription> devices;
1429 sock.SetTimeout(std::chrono::milliseconds(2000));
1430 const MaxTimeout timer{std::chrono::duration<double>(timeoutSec)};
1431
1432 while (!timer.TimedOut())
1433 {
1434 const Bytes data = sock.Recv();
1435 if (data.empty() || data.size() < 14)
1436 {
1437 continue;
1438 }
1439
1440 EthernetHeader eth;
1441 try
1442 {
1443 eth = EthernetHeader::Parse(data);
1444 }
1445 catch (const std::exception&)
1446 {
1447 continue;
1448 }
1449
1450 Bytes payload = eth.payload;
1452 if (eth.type == VLAN_ETHERTYPE)
1453 {
1454 if (payload.size() < 2 * sizeof(std::uint16_t))
1455 {
1456 continue;
1457 }
1458 r.Skip(sizeof(std::uint16_t));
1459 const auto innerType = r.U16();
1460 if (innerType != PROFINET_ETHERTYPE)
1461 {
1462 continue;
1463 }
1464 payload = Bytes(payload.begin() + (2 * sizeof(std::uint16_t)), payload.end());
1465 }
1466 else if (eth.type != PROFINET_ETHERTYPE)
1467 {
1468 continue;
1469 }
1470 if (payload.size() < 10)
1471 {
1472 continue;
1473 }
1474
1475 PNDCPHeader dcp;
1476 try
1477 {
1478 dcp = PNDCPHeader::Parse(payload);
1479 }
1480 catch (const std::exception&)
1481 {
1482 continue;
1483 }
1484
1485 if (dcp.serviceId != DcpServiceId::Hello)
1486 {
1487 continue;
1488 }
1489 if (dcp.serviceType != DcpServiceType::Request)
1490 {
1491 continue;
1492 }
1493
1494 std::map<BlockKey, Bytes> blocks;
1495 ForEachBlock(dcp.payload, [&](DcpOption option, std::uint8_t suboption, const Bytes& blockPayload)
1496 {
1497 blocks[MakeBlockKey(option, suboption)] = blockPayload;
1498 });
1499
1500 try
1501 {
1502 const DCPDeviceDescription device(eth.src, blocks);
1503 devices.push_back(device);
1504 if (callback)
1505 {
1506 callback(device);
1507 }
1508 }
1509 // NOLINTNEXTLINE(bugprone-empty-catch)
1510 catch (const std::exception&)
1511 {
1512 // Ignore malformed Hello PDUs.
1513 }
1514 }
1515
1516 return devices;
1517}
1518
1519bool SignalDevice(IRawEthernetSocket& sock, const MacAddress& src, const std::string& target,
1520 [[maybe_unused]] int durationMs, double timeoutSec)
1521{
1522 const MacAddress dst = String2Mac(target);
1523 const Bytes frame = BuildDcpSignalRequest(dst, src);
1524 sock.Send(frame);
1525
1526 try
1527 {
1528 const auto blockError = RecvSetResponse(sock, src, timeoutSec);
1529 if (blockError != DcpBlockError::Ok)
1530 {
1531 throw DCPError("DCP Signal failed: " + GetDcpBlockErrorDescription(blockError));
1532 }
1533 return true;
1534 }
1535 catch (const DCPTimeoutError&)
1536 {
1537 return false;
1538 }
1539}
1540
1541bool ResetToFactory(IRawEthernetSocket& sock, const MacAddress& src, const std::string& target,
1542 DcpResetMode mode, double timeoutSec)
1543{
1544 const MacAddress dst = String2Mac(target);
1545 const std::uint32_t xid = GenerateXid();
1546
1547 const auto modeValue = static_cast<std::uint16_t>(mode);
1548
1549 Bytes blockQualifier;
1550 wire::PutBigEndian(blockQualifier, modeValue);
1551
1552 PNDCPBlockRequest block;
1553 block.option = DcpOption::Control;
1554 block.suboption = static_cast<std::uint8_t>(DcpControlSuboption::ResetToFactory);
1555 block.length = static_cast<std::uint16_t>(blockQualifier.size());
1556 block.payload = blockQualifier;
1557
1558 PNDCPHeader dcp;
1559 dcp.frameId = DcpFrameId::GetSet;
1560 dcp.serviceId = DcpServiceId::Set;
1561 dcp.serviceType = DcpServiceType::Request;
1562 dcp.xId = xid;
1563 dcp.respDelay = 0;
1564 dcp.length = static_cast<std::uint16_t>(blockQualifier.size() + 4);
1565 dcp.payload = block.ToBytes();
1566
1567 sock.Send(BuildEthernetDcp(dst, src, dcp));
1568
1569 try
1570 {
1571 const auto blockError =
1572 RecvSetResponse(sock, src, timeoutSec);
1573 if (blockError != DcpBlockError::Ok)
1574 {
1575 throw DCPError("DCP Reset to Factory failed: " + GetDcpBlockErrorDescription(blockError));
1576 }
1577 std::this_thread::sleep_for(std::chrono::seconds(2));
1578 return true;
1579 }
1580 catch (const DCPTimeoutError&)
1581 {
1582 return false;
1583 }
1584}
1585
1586} // namespace profinet::dcp
DCP protocol errors.
Definition exceptions.h:539
DCP operation timed out.
Definition exceptions.h:551
Abstract interface for raw Ethernet frame transport.
virtual void Send(const std::vector< std::uint8_t > &frame)=0
Send one complete raw Ethernet frame.
virtual std::vector< std::uint8_t > Recv()=0
Receive one raw Ethernet frame.
virtual void SetTimeout(std::chrono::milliseconds timeout)=0
Set the receive timeout.
Time-limited-operation helper.
Definition util.h:267
Input validation error.
Definition exceptions.h:627
Parsed PROFINET device information from a DCP response.
Definition dcp.h:268
Lightweight cursor for parsing a big-endian byte buffer without copies.
Definition wire.h:192
std::uint8_t U8()
Read and consume one byte.
Definition wire.h:250
std::array< std::uint8_t, N > Fixed()
Read and consume a fixed-size byte array.
Definition wire.h:316
void Skip(std::size_t n)
Advance the read position without returning the skipped bytes.
Definition wire.h:330
std::uint16_t U16()
Read and consume a big-endian 16-bit value.
Definition wire.h:258
PROFINET DCP discovery and device configuration interface.
Declares exceptions and error types used by the PROFINET IO controller stack.
bool HasConflict(DcpIpBlockInfo info)
Whether an address conflict was detected.
Definition dcp.cpp:424
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
Bytes BuildSetParamRequest(const MacAddress &dst, const MacAddress &src, const std::string &param, const std::string &value, bool permanent)
Build a serialized DCP SET request for a named parameter.
Definition dcp.cpp:883
bool ResetToFactory(IRawEthernetSocket &sock, const MacAddress &src, const std::string &target, DcpResetMode mode=DcpResetMode::Communication, double timeoutSec=DEFAULT_TIMEOUT)
Reset a device to factory defaults.
Definition dcp.cpp:1541
std::string GetResetQualifierName(std::uint16_t qualifier)
Get the human-readable name for a reset qualifier value.
Definition dcp.cpp:447
void SendHello(IRawEthernetSocket &sock, const MacAddress &src, const std::string &stationName, const std::string &ip="0.0.0.0", const std::string &netmask="0.0.0.0", const std::string &gateway="0.0.0.0", std::pair< std::uint16_t, std::uint16_t > deviceId={0, 0}, DcpDeviceRole deviceRole=DcpDeviceRole::IoDevice)
Send a DCP Hello multicast announcement (device self-announce after power-on).
Definition dcp.cpp:1342
Bytes BuildDcpSignalRequest(const MacAddress &dst, const MacAddress &src)
Build a serialized DCP Signal request.
Definition dcp.cpp:937
bool SetIp(IRawEthernetSocket &sock, const MacAddress &src, const std::string &target, const std::string &ip, const std::string &netmask, const std::string &gateway, bool permanent=false, int timeoutSec=DEFAULT_TIMEOUT, int threadSleepMs=DEFAULT_THREAD_SLEEP_MS)
Set a device's IP configuration via DCP.
Definition dcp.cpp:1120
const std::map< std::string, BlockKey > & Params()
Table of supported parameter names, mapping to their (option, suboption).
Definition dcp.cpp:590
std::string DeviceRoleName(DcpDeviceRole role)
Get the human-readable name of a DCP device role.
Definition dcp.cpp:118
bool SignalDevice(IRawEthernetSocket &sock, const MacAddress &src, const std::string &target, int durationMs=FLASH_DURATION_MS, double timeoutSec=DEFAULT_TIMEOUT)
Flash a device's identification LEDs.
Definition dcp.cpp:1519
constexpr DcpBlockKey MakeBlockKey(std::uint16_t option, std::uint8_t suboption)
Definition dcp.h:231
DcpBlockError RecvSetResponse(IRawEthernetSocket &sock, const MacAddress &srcMac, int timeoutSec)
Receive and parse a DCP SET response, filtering out unrelated frames.
Definition dcp.cpp:253
std::string GetDeviceInitiativeName(DcpDeviceInitiative value)
Definition dcp.cpp:488
std::string GetDcpBlockQualifierDescription(DcpBlockQualifier qualifier)
Get the human-readable description of a DCP block qualifier.
Definition dcp.cpp:434
constexpr std::uint16_t DCP_IP_DHCP_BIT
Bit mask indicating DHCP bit is set.
Definition dcp.h:82
bool IsDhcp(DcpIpBlockInfo info)
Whether the address was assigned via DHCP.
Definition dcp.cpp:429
constexpr std::uint16_t blockNameVendorRangeMin
Default flash duration of the leds.
Definition dcp.h:75
constexpr std::uint16_t blockNameVendorRangeMax
Default flash duration of the leds.
Definition dcp.h:77
void SendRequest(IRawEthernetSocket &sock, const MacAddress &src, BlockKey blockType, const std::vector< std::uint8_t > &value)
Send a DCP Identify request filtered to a specific (option, suboption).
Definition dcp.cpp:1195
const std::string DCP_MULTICAST_MAC
DCP Identify/Get/Set multicast destination MAC.
Definition dcp.h:55
const std::string DCP_HELLO_MULTICAST_MAC
DCP Hello multicast destination MAC.
Definition dcp.h:57
std::string GetIPBlockInfoName(DcpIpBlockInfo info)
Get the human-readable name for an IP block info value.
Definition dcp.cpp:402
std::vector< std::string > DecodeDeviceRole(std::uint8_t roleByte)
Decode a device role bitmask into a list of role names.
Definition dcp.cpp:97
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
std::string GetBlockName(std::uint16_t option, std::uint8_t suboption)
Get the human-readable name for a (option, suboption) DCP block.
Definition dcp.cpp:196
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
constexpr std::uint16_t DCP_IP_CONFLICT_BIT
Bit mask indicating an IP address conflict.
Definition dcp.h:80
std::string GetDcpBlockErrorDescription(DcpBlockError error)
Get the human-readable name for a DCP SET block-level error code.
Definition dcp.cpp:524
std::string GetSuboptionName(std::uint16_t option, std::uint8_t value)
Definition dcp.cpp:125
std::string GetDcpResponseCodeName(DcpResponseCode value)
Get the human-readable name for a device-response value.
Definition dcp.cpp:501
bool SetParam(IRawEthernetSocket &sock, const MacAddress &src, const std::string &target, const std::string &param, const std::string &value, int timeoutSec=DEFAULT_TIMEOUT, bool permanent=false, int threadSleepMs=DEFAULT_THREAD_SLEEP_MS)
Write a named parameter to a device.
Definition dcp.cpp:1096
DcpBlockError ParseSetResponse(const Bytes &data)
Parses the raw byte response payload received from a SET operation.
Definition dcp.cpp:319
std::vector< DCPDeviceDescription > ReceiveHello(IRawEthernetSocket &sock, const MacAddress &myMac, double timeoutSec=DEFAULT_HELLO_TIMEOUT, const std::function< void(const DCPDeviceDescription &)> &callback=nullptr)
Listen for DCP Hello announcements from other devices.
Definition dcp.cpp:1424
constexpr std::size_t DCP_MAX_NAME_LENGTH
Maximum allowed length in bytes for a station name.
Definition dcp.h:60
std::string Hex2(std::uint8_t value)
Format an 8-bit unsigned integer as a 2-digit hexadecimal string.
Definition util.cpp:49
DcpBlockQualifier
DCP block qualifier values.
Definition protocol.h:430
DcpIpSuboption
DCP IP suboptions.
Definition protocol.h:147
DcpDeviceInitiativeSuboption
DCP DeviceInitiative suboptions.
Definition protocol.h:256
DcpDhcpSuboption
DCP DHCP suboptions.
Definition protocol.h:222
constexpr std::uint16_t VLAN_ETHERTYPE
EtherType value identifying an 802.1Q VLAN tag (0x8100).
Definition util.h:34
constexpr int uuidLenght
Constant lenght of a UUID.
Definition util.h:40
DcpControlSuboption
DCP Control suboptions.
Definition protocol.h:197
@ Response
Control response.
std::vector< std::uint8_t > ToVec(const std::string &input)
Convert a string into a vector of raw bytes.
Definition util.cpp:54
DcpBlockError
DCP block-level SET response errors.
Definition protocol.h:331
std::string String2Ip(std::span< const std::uint8_t > ipBytes)
Format raw IPv4 address bytes as a dotted string.
Definition util.cpp:206
std::array< std::uint8_t, macAddressLength > MacAddress
A 6-byte Ethernet MAC address.
Definition util.h:48
std::array< std::uint8_t, 4 > Ip2String(const std::string &ipStr)
Parse a dotted-decimal IPv4 address string.
Definition util.cpp:222
DcpResetMode
Reset mode bitmask.
Definition protocol.h:289
MacAddress String2Mac(const std::string &macStr)
Parse a colon-separated MAC address string.
Definition util.cpp:122
constexpr int macAddressLength
Constant lenght of a mac address.
Definition util.h:37
std::string ToHex(const std::uint8_t *data, std::size_t len)
Hex-encode a byte buffer.
Definition util.cpp:104
DcpOption
DCP options.
Definition protocol.h:116
@ Nme
Network Management.
@ Unknown
Not set / initialize value.
@ All
Wildcard: all options.
@ DeviceInitiative
Device initiative.
@ Device
Device identification.
@ Ip
IP configuration.
constexpr std::uint16_t PROFINET_ETHERTYPE
EtherType value identifying PROFINET frames (0x8892).
Definition util.h:30
DcpDeviceSuboption
DCP Device suboptions.
Definition protocol.h:163
@ DeviceId
Vendor and device ID.
DcpDeviceInitiative
DCP device-initiative values.
Definition protocol.h:321
std::string Hex4(std::uint16_t value)
Format a 16-bit unsigned integer as a 4-digit hexadecimal string.
Definition util.cpp:44
std::vector< std::uint8_t > Bytes
Generic byte buffer alias used throughout the library for raw wire data.
Definition protocol.h:55
DcpResetQualifier
Reset qualifier values for ResetToFactory().
Definition protocol.h:381
std::string Mac2String(const MacAddress &mac)
Format a MAC address as a colon-separated string.
Definition util.cpp:188
@ ResponseUnsupported
Service unsupported response.
@ ResponseSuccess
Successful response.
DcpIpBlockInfo
PROFINET DCP IP block status values. IP block status flags (IEC 61158-6-10 IPBlockInfo).
Definition protocol.h:440
DcpDeviceRole
Device role bitmask.
Definition protocol.h:263
std::string DecodeBytes(const std::uint8_t *data, std::size_t len)
Decode bytes to a UTF-8 string, stripping trailing NUL bytes.
Definition util.cpp:246
constexpr std::uint8_t ByteAt(T value, std::size_t index)
Return one byte from an integral value.
Definition util.h:244
std::string GetVendorName(std::uint16_t vendorId)
Get the vendor name for a 16-bit PROFINET vendor ID.
Definition vendors.cpp:29
DcpResponseCode
DCP GET/SET response codes.
Definition protocol.h:356
PROFINET protocol wire-format structures and serialization helpers.
asio::error_code error
asio::steady_timer timer
Identifies a PROFINET DCP block by option and suboption.
Definition protocol.h:465
std::uint8_t suboption
DCP suboption code.
Definition protocol.h:470
DcpOption option
DCP option.
Definition protocol.h:467
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
Bytes ToBytes() const
Serialize this header and its payload back to raw frame bytes.
Definition protocol.h:513
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
Parsed DHCP block from a DCP response.
Definition dcp.h:195
std::optional< std::string > fqdn
Decoded fully qualified domain name, if this is an FQDN suboption.
Definition dcp.h:215
std::vector< std::uint8_t > rawData
Raw, undecoded block payload.
Definition dcp.h:203
DcpDhcpSuboption suboption
DHCP suboption code.
Definition dcp.h:197
std::optional< std::vector< std::uint8_t > > vendorSpecific
Decoded vendor-specific data, if this is a VendorSpec suboption.
Definition dcp.h:212
std::optional< std::string > hostname
Decoded hostname, if this is a Hostname suboption.
Definition dcp.h:206
std::string suboptionName
Human-readable name of the suboption.
Definition dcp.h:200
std::optional< std::vector< std::uint8_t > > clientId
Decoded client ID, if this is a ClientID suboption.
Definition dcp.h:209
std::optional< std::string > uuid
Decoded UUID string, if this is a UUID suboption.
Definition dcp.h:218
Bytes frame
std::uint8_t suboption
Definition test_dcp.cpp:390
Bytes payload
Definition test_dcp.cpp:392
std::uint16_t length
Definition test_dcp.cpp:391
DcpOption option
Definition test_dcp.cpp:389
Declares PROFINET Vendor ID lookup utilities. The lookup table itself lives in vendors_data....