PROFINET IO Controller Stack 1.0.0
Modern C++ implementation of a PROFINET IO Controller stack
Loading...
Searching...
No Matches
test_cyclicCapture.cpp
Go to the documentation of this file.
1
21
22#include <gtest/gtest.h>
23
24#include <algorithm>
25#include <array>
26#include <condition_variable>
27#include <cstdint>
28#include <filesystem>
29#include <fstream>
30#include <functional>
31#include <iterator>
32#include <memory>
33#include <mutex>
34#include <numeric>
35#include <optional>
36#include <stdexcept>
37#include <string>
38#include <tuple>
39#include <utility>
40#include <vector>
41
43#include "profinet/cyclic.h"
44#include "profinet/gsdml.h"
45#include "profinet/protocol.h"
46#include "profinet/rpc.h"
47#include "profinet/rt.h"
48#include "profinet/util.h"
50
52{
53namespace
54{
55using Mac = std::array<std::uint8_t, 6>;
56
57constexpr Mac kPhoenixMac{0xA8, 0x74, 0x1D, 0x44, 0x7A, 0xBB};
58constexpr Mac kAumaMac{0xA0, 0xD3, 0x85, 0x00, 0xF6, 0x13};
59
60constexpr std::size_t kEthernetHeaderLength = 14U;
61constexpr std::size_t kRtHeaderFrameIdLength = 2U;
62constexpr std::size_t kOutputPayloadLength = 40U;
63constexpr std::size_t kInputPayloadLength = 43U;
64constexpr std::size_t kCommandOffset = 8U;
65constexpr std::size_t kPositionOffset = 3U;
66
67constexpr std::uint8_t kStopCommand = 0x00;
68constexpr std::uint8_t kOpenCommand = 0x01;
69constexpr std::uint8_t kCloseCommand = 0x02;
70
71constexpr std::uint8_t kOutputDataStatus = 0x35;
72constexpr std::uint8_t kInputDataStatus = 0x15;
73
74constexpr std::uint32_t kExpectedCycleTimeUs = 8000U;
75constexpr double kExpectedCommandDurationMs = 1000.0;
76constexpr double kCommandDurationToleranceMs = 5.0;
77
78constexpr char kAumaGsdml[] = "GSDML-V2.42-AUMA-SGX-20220920.xml";
79constexpr char kPhoenixGsdml[] =
80 "GSDML-V2.45-Phoenix Contact-AXC F 1252-20250813.xml";
81
82struct CapturedPacket
83{
84 std::uint64_t timestampNs = 0U;
86};
87
88struct Capture
89{
90 std::vector<CapturedPacket> phoenixToAuma;
91 std::vector<CapturedPacket> aumaToPhoenix;
92};
93
94std::uint32_t ReadU32(
95 const Bytes& bytes,
96 std::size_t offset,
97 bool littleEndian)
98{
99 if (offset + 4U > bytes.size())
100 {
101 throw std::runtime_error("PCAPNG u32 read out of range");
102 }
103
104 if (littleEndian)
105 {
106 return static_cast<std::uint32_t>(bytes[offset]) |
107 (static_cast<std::uint32_t>(bytes[offset + 1U]) << 8U) |
108 (static_cast<std::uint32_t>(bytes[offset + 2U]) << 16U) |
109 (static_cast<std::uint32_t>(bytes[offset + 3U]) << 24U);
110 }
111
112 return (static_cast<std::uint32_t>(bytes[offset]) << 24U) |
113 (static_cast<std::uint32_t>(bytes[offset + 1U]) << 16U) |
114 (static_cast<std::uint32_t>(bytes[offset + 2U]) << 8U) |
115 static_cast<std::uint32_t>(bytes[offset + 3U]);
116}
117
119std::uint64_t TimestampToNs(
120 std::uint64_t ticks,
121 std::uint8_t resolution)
122{
123 if ((resolution & 0x80U) == 0U)
124 {
125 const auto exponent = static_cast<unsigned>(resolution);
126 std::uint64_t divisor = 1U;
127 for (unsigned index = 0U; index < exponent; ++index)
128 {
129 divisor *= 10U;
130 }
131
132 return static_cast<std::uint64_t>(
133 static_cast<long double>(ticks) * 1'000'000'000.0L /
134 static_cast<long double>(divisor));
135 }
136
137 const auto exponent = static_cast<unsigned>(resolution & 0x7FU);
138 return static_cast<std::uint64_t>(
139 static_cast<long double>(ticks) * 1'000'000'000.0L /
140 static_cast<long double>(std::uint64_t{1} << exponent));
141}
142
144std::filesystem::path CapturePath(const std::string& fileName)
145{
146 const std::array<std::filesystem::path, 5> candidates{
147 GetExecutableDirectory() / "captures" / fileName,
148 GetExecutableDirectory() / fileName,
149 std::filesystem::current_path() / "tests" / "captures" / fileName,
150 std::filesystem::current_path() / "captures" / fileName,
151 std::filesystem::current_path() / fileName};
152
153 for (const auto& candidate : candidates)
154 {
155 if (std::filesystem::exists(candidate))
156 {
157 return candidate;
158 }
159 }
160
161 ADD_FAILURE() << "PCAPNG fixture not found: " << fileName;
162 return {};
163}
164
166std::vector<CapturedPacket> ReadPcapng(const std::filesystem::path& path)
167{
168 std::ifstream file(path, std::ios::binary);
169 if (!file)
170 {
171 throw std::runtime_error("Cannot open PCAPNG fixture: " + path.string());
172 }
173
174 const Bytes bytes{
175 std::istreambuf_iterator<char>(file),
176 std::istreambuf_iterator<char>()};
177
178 if (bytes.size() < 12U)
179 {
180 throw std::runtime_error("PCAPNG fixture is too short: " + path.string());
181 }
182
183 const auto byteOrderMagic = ReadU32(bytes, 8U, true);
184 const bool littleEndian = byteOrderMagic == 0x1A2B3C4DU;
185 if (!littleEndian && byteOrderMagic != 0x4D3C2B1AU)
186 {
187 throw std::runtime_error("Unsupported PCAPNG byte-order magic");
188 }
189
190 std::vector<std::uint8_t> timestampResolution{6U};
191 std::vector<CapturedPacket> packets;
192
193 std::size_t offset = 0U;
194 while (offset + 12U <= bytes.size())
195 {
196 const auto blockType = ReadU32(bytes, offset, littleEndian);
197 const auto blockLength = ReadU32(bytes, offset + 4U, littleEndian);
198
199 if (blockLength < 12U || offset + blockLength > bytes.size())
200 {
201 throw std::runtime_error("Invalid PCAPNG block length");
202 }
203
204 if (blockType == 0x00000001U && blockLength >= 20U)
205 {
207 std::size_t optionOffset = offset + 16U;
208 const std::size_t optionEnd = offset + blockLength - 4U;
209
210 while (optionOffset + 4U <= optionEnd)
211 {
212 const auto code = static_cast<std::uint16_t>(
213 ReadU32(bytes, optionOffset, littleEndian) & 0xFFFFU);
214 const auto optionLength = static_cast<std::uint16_t>(
215 (ReadU32(bytes, optionOffset, littleEndian) >> 16U) & 0xFFFFU);
216
217 optionOffset += 4U;
218 if (code == 0U)
219 {
220 break;
221 }
222
223 if (optionOffset + optionLength > optionEnd)
224 {
225 break;
226 }
227
228 if (code == 9U && optionLength >= 1U)
229 {
230 timestampResolution[0] = bytes[optionOffset];
231 }
232
233 optionOffset += ((optionLength + 3U) / 4U) * 4U;
234 }
235 }
236 else if (blockType == 0x00000006U && blockLength >= 32U)
237 {
238 const auto interfaceId = ReadU32(bytes, offset + 8U, littleEndian);
239 if (interfaceId >= timestampResolution.size())
240 {
241 throw std::runtime_error("PCAPNG interface id out of range");
242 }
243
244 const auto timestampHigh = ReadU32(bytes, offset + 12U, littleEndian);
245 const auto timestampLow = ReadU32(bytes, offset + 16U, littleEndian);
246 const auto capturedLength = ReadU32(bytes, offset + 20U, littleEndian);
247
248 const auto packetOffset = offset + 28U;
249 if (packetOffset + capturedLength > offset + blockLength - 4U)
250 {
251 throw std::runtime_error("PCAPNG packet exceeds its block");
252 }
253
254 const auto ticks =
255 (static_cast<std::uint64_t>(timestampHigh) << 32U) |
256 static_cast<std::uint64_t>(timestampLow);
257
258 CapturedPacket packet;
259 packet.timestampNs =
260 TimestampToNs(ticks, timestampResolution[interfaceId]);
261 packet.frame.assign(
262 bytes.begin() + packetOffset,
263 bytes.begin() +
264 packetOffset + capturedLength);
265 packets.push_back(std::move(packet));
266 }
267
268 offset += blockLength;
269 }
270
271 return packets;
272}
273
275bool IsDirection(
276 const Bytes& frame,
277 const Mac& source,
278 const Mac& destination)
279{
280 return frame.size() >= kEthernetHeaderLength + kRtHeaderFrameIdLength &&
281 std::equal(destination.begin(), destination.end(), frame.begin()) &&
282 std::equal(source.begin(), source.end(), frame.begin() + 6) &&
283 frame[12] == 0x88U &&
284 frame[13] == 0x92U;
285}
286
288Capture LoadCapture(const std::string& fileName)
289{
290 const auto packets = ReadPcapng(CapturePath(fileName));
291
292 Capture capture;
293 for (const auto& packet : packets)
294 {
295 if (IsDirection(packet.frame, kPhoenixMac, kAumaMac))
296 {
297 capture.phoenixToAuma.push_back(packet);
298 }
299 else if (IsDirection(packet.frame, kAumaMac, kPhoenixMac))
300 {
301 capture.aumaToPhoenix.push_back(packet);
302 }
303 }
304
305 return capture;
306}
307
309rt::RTFrame ParseCapturedRtFrame(const CapturedPacket& packet)
310{
311 const auto parsed = rt::ParseEthernetFrame(packet.frame);
312 if (!parsed.has_value())
313 {
314 throw std::runtime_error("Captured frame is not a valid PROFINET RT frame");
315 }
316 return *parsed;
317}
318
320std::uint8_t CommandByte(const CapturedPacket& packet)
321{
322 return ParseCapturedRtFrame(packet).payload.at(kCommandOffset);
323}
324
326std::uint16_t Position(const CapturedPacket& packet)
327{
328 const auto payload = ParseCapturedRtFrame(packet).payload;
329 return static_cast<std::uint16_t>(
330 (static_cast<std::uint16_t>(payload.at(kPositionOffset)) << 8U) |
331 static_cast<std::uint16_t>(payload.at(kPositionOffset + 1U)));
332}
333
335std::vector<std::tuple<std::size_t, std::uint8_t, std::uint8_t>> CommandTransitions(
336 const std::vector<CapturedPacket>& packets)
337{
338 std::vector<std::tuple<std::size_t, std::uint8_t, std::uint8_t>> transitions;
339 if (packets.empty())
340 {
341 return transitions;
342 }
343
344 auto previous = CommandByte(packets.front());
345 for (std::size_t index = 1U; index < packets.size(); ++index)
346 {
347 const auto current = CommandByte(packets[index]);
348 if (current != previous)
349 {
350 transitions.emplace_back(index, previous, current);
351 previous = current;
352 }
353 }
354 return transitions;
355}
356
358std::vector<std::pair<std::size_t, std::uint16_t>> PositionTransitions(
359 const std::vector<CapturedPacket>& packets)
360{
361 std::vector<std::pair<std::size_t, std::uint16_t>> transitions;
362 if (packets.empty())
363 {
364 return transitions;
365 }
366
367 auto previous = Position(packets.front());
368 for (std::size_t index = 1U; index < packets.size(); ++index)
369 {
370 const auto current = Position(packets[index]);
371 if (current != previous)
372 {
373 transitions.emplace_back(index, current);
374 previous = current;
375 }
376 }
377 return transitions;
378}
379
381double AveragePeriodUs(const std::vector<CapturedPacket>& packets)
382{
383 if (packets.size() < 2U)
384 {
385 return 0.0;
386 }
387
388 std::uint64_t sumNs = 0U;
389 for (std::size_t index = 1U; index < packets.size(); ++index)
390 {
391 sumNs += packets[index].timestampNs - packets[index - 1U].timestampNs;
392 }
393
394 return static_cast<double>(sumNs) /
395 static_cast<double>(packets.size() - 1U) /
396 1000.0;
397}
398
400gsdml::GSDMLDevice LoadGsdml(const std::string& fileName)
401{
402 const std::array<std::filesystem::path, 4> candidates{
403 GetExecutableDirectory() / "gsdml" / fileName,
404 GetExecutableDirectory() / "tests" / "gsdml" / fileName,
405 std::filesystem::current_path() / "tests" / "gsdml" / fileName,
406 std::filesystem::current_path() / "gsdml" / fileName};
407
408 for (const auto& candidate : candidates)
409 {
410 if (std::filesystem::exists(candidate))
411 {
412 return gsdml::ParseGsdml(candidate.string());
413 }
414 }
415
416 ADD_FAILURE() << "GSDML fixture not found: " << fileName;
417 return {};
418}
419
421const rpc::IOSlot* FindSlot(
422 const std::vector<rpc::IOSlot>& slots,
423 std::uint16_t slot,
424 std::uint16_t subslot)
425{
426 const auto it = std::find_if(
427 slots.begin(),
428 slots.end(),
429 [slot, subslot](const rpc::IOSlot& value)
430 {
431 return value.slot == slot && value.subslot == subslot;
432 });
433
434 return it == slots.end() ? nullptr : &(*it);
435}
436
444std::vector<rpc::IOSlot> PhoenixAumaCapturedSlots()
445{
446 std::vector<rpc::IOSlot> slots;
447
448 const auto addDap = [&slots](
449 std::uint16_t subslot,
450 std::uint32_t moduleIdent,
451 std::uint32_t submoduleIdent)
452 {
453 rpc::IOSlot slot;
454 slot.slot = 0;
455 slot.subslot = subslot;
456 slot.moduleIdent = moduleIdent;
457 slot.submoduleIdent = submoduleIdent;
458 slots.push_back(slot);
459 };
460
461 // These identifiers are the DAP/PDEV submodule identifiers represented in
462 // the captured ExpectedSubmoduleBlockReq.
463 addDap(0x0001, 0x00008001U, 0x00000000U);
464 addDap(0x0002, 0x00000010U, 0x00000001U);
465 addDap(0x0003, 0x00000010U, 0x00000001U);
466 addDap(0x0004, 0x00000010U, 0x00000001U);
467 addDap(0x8000, 0x00000010U, 0x00000001U);
468 addDap(0x8001, 0x00000003U, 0x00000001U);
469 addDap(0x8002, 0x00000003U, 0x00000001U);
470
471 rpc::IOSlot input;
472 input.slot = 1;
473 input.subslot = 1;
474 input.inputLength = 34;
475 input.moduleIdent = 0x00008000U;
476 input.submoduleIdent = 0x00002200U;
477 slots.push_back(input);
478
479 rpc::IOSlot output;
480 output.slot = 2;
481 output.subslot = 1;
482 output.outputLength = 16;
483 output.moduleIdent = 0x00008100U;
484 output.submoduleIdent = 0x10001000U;
485 slots.push_back(output);
486
487 return slots;
488}
489
491rt::IOCRConfig CapturedInputIocr()
492{
493 rt::IOCRConfig config;
494 config.typeIOCR = rt::IOCR_TYPE_INPUT;
495 config.referenceIOCR = 1;
496 config.frameId = kInputFrameId;
497 config.sendClockFactor = 32;
498 config.reductionRatio = 8;
499 config.phase = 8;
500 config.watchdogFactor = 3;
501 config.dataLength = 43;
502 config.objects = {
503 {0, 0x0001, 36, 0, 0, 0},
504 {0, 0x0002, 37, 0, 0, 0},
505 {0, 0x0003, 38, 0, 0, 0},
506 {0, 0x0004, 39, 0, 0, 0},
507 {0, 0x8000, 40, 0, 0, 0},
508 {0, 0x8001, 41, 0, 0, 0},
509 {0, 0x8002, 42, 0, 0, 0},
510 {1, 1, 1, 34, 35, 0},
511 {2, 1, 0, 0, 0, 0}};
512 config.objects.back().offsetIOCS = 0;
513 return config;
514}
515
517rt::IOCRConfig CapturedOutputIocr()
518{
519 rt::IOCRConfig config;
520 config.typeIOCR = rt::IOCR_TYPE_OUTPUT;
521 config.referenceIOCR = 2;
522 config.frameId = kOutputFrameId;
523 config.sendClockFactor = 32;
524 config.reductionRatio = 8;
525 config.phase = 8;
526 config.watchdogFactor = 3;
527 config.dataLength = 40;
528 config.objects = {
529 {0, 0x0001, 0, 0, 0, 0},
530 {0, 0x0002, 1, 0, 0, 0},
531 {0, 0x0003, 2, 0, 0, 0},
532 {0, 0x0004, 3, 0, 0, 0},
533 {0, 0x8000, 4, 0, 0, 0},
534 {0, 0x8001, 5, 0, 0, 0},
535 {0, 0x8002, 6, 0, 0, 0},
536 {1, 1, 7, 0, 0, 0},
537 {2, 1, 8, 16, 24, 0}};
538 return config;
539}
540
542std::vector<Bytes> ExtractBlocks(const Bytes& payload)
543{
544 std::vector<Bytes> result;
545 std::size_t offset = 0U;
546 while (offset + blockHeaderLength <= payload.size())
547 {
548 const auto header = rpc::PeekBlockHeader(payload, offset);
549 const std::size_t blockSize = 4U + header.blockLength;
550 if (blockSize > payload.size() - offset)
551 {
552 break;
553 }
554
555 result.emplace_back(
556 payload.begin() + offset,
557 payload.begin() + offset + blockSize);
558 offset += blockSize;
559 }
560 return result;
561}
562
564std::vector<Bytes> CapturedConnectBlocks(std::uint16_t captureThisBlock)
565{
566 const auto packets = ReadPcapng(CapturePath("phoenix_to_auma.pcapng"));
567 std::vector<Bytes> blocks;
568
569 for (const auto& packet : packets)
570 {
571 // The CONNECT request contains the distinctive ARBlockReq prefix.
572 const Bytes marker{0x01, 0x01, 0x00, 0x43};
573 const auto markerIt = std::search(
574 packet.frame.begin(),
575 packet.frame.end(),
576 marker.begin(),
577 marker.end());
578
579 if (markerIt == packet.frame.end())
580 {
581 continue;
582 }
583
584 const std::size_t start = static_cast<std::size_t>(
585 std::distance(packet.frame.begin(), markerIt));
586
587 std::size_t offset = start;
588 while (offset + blockHeaderLength <= packet.frame.size())
589 {
590 const auto header = rpc::PeekBlockHeader(packet.frame, offset);
591 const std::size_t blockSize = 4U + header.blockLength;
592
593 if (blockSize > packet.frame.size() - offset)
594 {
595 break;
596 }
597
598 if (header.blockType == static_cast<std::uint16_t>(BlockType::ApplicationRelationRequest))
599 {
600 blocks.emplace_back(
601 packet.frame.begin() + offset,
602 packet.frame.begin() +
603 offset + blockSize);
604 }
605 if (header.blockType == captureThisBlock)
606 {
607 blocks.emplace_back(
608 packet.frame.begin() + offset,
609 packet.frame.begin() +
610 offset + blockSize);
611 }
612 offset += blockSize;
613 }
614 }
615 if (!blocks.empty())
616 {
617 return blocks;
618 }
619 ADD_FAILURE() << "CONNECT request blocks were not found in phoenix_to_auma.pcapng";
620 return {};
621}
622
624const Bytes* FindBlock(
625 const std::vector<Bytes>& blocks,
626 std::uint16_t blockType,
627 std::size_t occurrence = 0U)
628{
629 std::size_t count = 0U;
630 for (const auto& block : blocks)
631 {
632 if (rpc::PeekBlockHeader(block, 0U).blockType == blockType)
633 {
634 if (count == occurrence)
635 {
636 return &block;
637 }
638 ++count;
639 }
640 }
641 return nullptr;
642}
643
645dcp::DCPDeviceDescription AumaRpcInfo()
646{
647 dcp::DCPDeviceDescription info(kAumaMac, {});
648 info.name = "axcf1252-lan1";
649 info.ip = "127.0.0.1";
650 info.vendorHigh = 0x01;
651 info.vendorLow = 0x3F;
652 info.deviceHigh = 0x00;
653 info.deviceLow = 0x02;
654 return info;
655}
656
658class CyclicControllerCapture
659{
660 public:
662 explicit CyclicControllerCapture(
663 rt::IOCRConfig input,
664 rt::IOCRConfig output)
665 {
666 tx = std::make_unique<testing::NiceMock<MockRawEthernetSocket>>();
667 rx = std::make_unique<testing::NiceMock<MockRawEthernetSocket>>();
668
669 txSocket = tx.get();
670 rxSocket = rx.get();
671
672 ON_CALL(*txSocket, Send(testing::_))
673 .WillByDefault([this](const Bytes& frame)
674 {
675 {
676 const std::lock_guard lock(mutex);
677 sentFrames.push_back(frame);
678 }
679 condition.notify_all();
680 });
681
682 ON_CALL(*rxSocket, Recv())
683 .WillByDefault([this]
684 {
685 const std::lock_guard lock(mutex);
686 return rxFrame;
687 });
688
689 ON_CALL(*rxSocket, SetTimeout(testing::_))
690 .WillByDefault([](std::chrono::milliseconds) {});
691 ioContext = std::make_unique<asio::io_context>();
692
693 controller = std::make_unique<cyclic::CyclicController>(
694 *ioContext,
695 "test0",
696 kPhoenixMac,
697 kAumaMac,
698 std::move(input),
699 std::move(output),
700 3,
702 }
703
705 void SetRxFrame(Bytes frame)
706 {
707 const std::lock_guard lock(mutex);
708 rxFrame = std::move(frame);
709 }
710
712 void Start()
713 {
714 controller->Start(std::move(tx), std::move(rx));
715 }
716
718 void Stop()
719 {
720 controller->Stop();
721 }
722
724 void SetOutputData(
725 int slot,
726 int subslot,
727 const Bytes& data)
728 {
729 controller->SetOutputData(slot, subslot, data);
730 }
731
733 void OnInput(std::function<void(int, int, const Bytes&)> callback)
734 {
735 controller->OnInput(std::move(callback));
736 }
737
739 template <typename Predicate>
740 bool WaitForSent(Predicate&& predicate, std::chrono::milliseconds timeout)
741 {
742 std::unique_lock lock(mutex);
743 return condition.wait_for(
744 lock,
745 timeout,
746 [&]
747 {
748 return std::ranges::any_of(sentFrames, predicate);
749 });
750 }
751
753 std::vector<Bytes> SentFrames() const
754 {
755 const std::lock_guard lock(mutex);
756 return sentFrames;
757 }
758
759 private:
760 std::unique_ptr<cyclic::CyclicController> controller;
761
762 std::unique_ptr<MockRawEthernetSocket> tx;
763 std::unique_ptr<MockRawEthernetSocket> rx;
764
767
768 mutable std::mutex mutex;
769 std::condition_variable condition;
771 std::vector<Bytes> sentFrames;
772
774 std::unique_ptr<asio::io_context> ioContext;
775};
776
777} // namespace
778
780TEST(CyclicCaptureTests, PhoenixToAumaCaptureHasExpectedCyclicContract)
781{
782 const auto capture = LoadCapture("phoenix_to_auma.pcapng");
783
784 ASSERT_EQ(capture.phoenixToAuma.size(), 399U);
785 ASSERT_EQ(capture.aumaToPhoenix.size(), 399U);
786
787 const auto phoenixRt = std::count_if(
788 capture.phoenixToAuma.begin(),
789 capture.phoenixToAuma.end(),
790 [](const CapturedPacket& packet)
791 {
792 return packet.frame.size() >= 16U &&
793 ParseCapturedRtFrame(packet).frameId == kOutputFrameId;
794 });
795
796 const auto aumaRt = std::count_if(
797 capture.aumaToPhoenix.begin(),
798 capture.aumaToPhoenix.end(),
799 [](const CapturedPacket& packet)
800 {
801 return packet.frame.size() >= 16U &&
802 ParseCapturedRtFrame(packet).frameId == kInputFrameId;
803 });
804
805 EXPECT_EQ(phoenixRt, 399);
806 EXPECT_EQ(aumaRt, 398);
807
808 const auto firstAumaFrame = ParseCapturedRtFrame(capture.aumaToPhoenix.front());
809 EXPECT_EQ(firstAumaFrame.frameId, 0xFEFF);
810 EXPECT_EQ(firstAumaFrame.payload.size(), 102U);
811
812 for (const auto& packet : capture.phoenixToAuma)
813 {
814 const auto frame = ParseCapturedRtFrame(packet);
815 EXPECT_EQ(frame.frameId, kOutputFrameId);
816 EXPECT_EQ(frame.payload.size(), kOutputPayloadLength);
817 EXPECT_TRUE(frame.dataStatus | kOutputDataStatus);
818 EXPECT_EQ(frame.transferStatus, 0U);
819 EXPECT_EQ(packet.frame.size(), 60U);
820 }
821
822 for (std::size_t index = 1U; index < capture.aumaToPhoenix.size(); ++index)
823 {
824 const auto frame = ParseCapturedRtFrame(capture.aumaToPhoenix[index]);
825 EXPECT_EQ(frame.frameId, kInputFrameId);
826 EXPECT_EQ(frame.payload.size(), kInputPayloadLength);
827 EXPECT_TRUE(frame.dataStatus | kInputDataStatus);
828 EXPECT_EQ(frame.transferStatus, 0U);
829 EXPECT_EQ(capture.aumaToPhoenix[index].frame.size(), 63U);
830 }
831
832 EXPECT_NEAR(
833 AveragePeriodUs(capture.phoenixToAuma),
834 8000.0,
835 500.0);
836}
837
839TEST(CyclicCaptureTests, SetValveOpenContainsOneSecondOpenCommand)
840{
841 const auto capture = LoadCapture("Set valve open.pcapng");
842
843 ASSERT_EQ(capture.aumaToPhoenix.size(), 626U);
844 ASSERT_EQ(capture.phoenixToAuma.size(), 627U);
845
846 for (const auto& packet : capture.phoenixToAuma)
847 {
848 const auto frame = ParseCapturedRtFrame(packet);
849 EXPECT_EQ(frame.frameId, kOutputFrameId);
850 EXPECT_EQ(frame.payload.size(), kOutputPayloadLength);
851 EXPECT_EQ(frame.dataStatus, kOutputDataStatus);
852 EXPECT_EQ(frame.transferStatus, 0U);
853 EXPECT_EQ(packet.frame.size(), 60U);
854 }
855
856 for (const auto& packet : capture.aumaToPhoenix)
857 {
858 const auto frame = ParseCapturedRtFrame(packet);
859 EXPECT_EQ(frame.frameId, kInputFrameId);
860 EXPECT_EQ(frame.payload.size(), kInputPayloadLength);
861 EXPECT_EQ(frame.dataStatus, kInputDataStatus);
862 EXPECT_EQ(frame.transferStatus, 0U);
863 EXPECT_EQ(packet.frame.size(), 63U);
864 }
865
866 const auto transitions = CommandTransitions(capture.phoenixToAuma);
867 const std::vector<std::tuple<std::size_t, std::uint8_t, std::uint8_t>>
868 expected{{196U, kStopCommand, kOpenCommand},
869 {321U, kOpenCommand, kStopCommand}};
870 EXPECT_EQ(transitions, expected);
871
872 const auto startPacket = capture.phoenixToAuma[196U];
873 const auto stopPacket = capture.phoenixToAuma[321U];
874 const auto durationMs =
875 static_cast<double>(stopPacket.timestampNs - startPacket.timestampNs) /
876 1'000'000.0;
877
878 EXPECT_NEAR(
879 durationMs,
880 kExpectedCommandDurationMs,
881 kCommandDurationToleranceMs);
882
883 EXPECT_EQ(CommandByte(startPacket), kOpenCommand);
884 EXPECT_EQ(CommandByte(stopPacket), kStopCommand);
885
886 const auto positionTransitions = PositionTransitions(capture.aumaToPhoenix);
887 const std::vector<std::pair<std::size_t, std::uint16_t>> expectedPositions{
888 {232U, 0x0206U},
889 {264U, 0x0207U},
890 {279U, 0x020FU},
891 {298U, 0x0217U},
892 {311U, 0x0221U},
893 {330U, 0x0230U},
894 {348U, 0x0232U}};
895 EXPECT_EQ(positionTransitions, expectedPositions);
896
897 std::uint16_t previous = Position(capture.aumaToPhoenix.front());
898 EXPECT_EQ(previous, 0x0202U);
899 for (std::size_t index = 1U; index < capture.aumaToPhoenix.size(); ++index)
900 {
901 const auto current = Position(capture.aumaToPhoenix[index]);
902 EXPECT_GE(current, previous);
903 previous = current;
904 }
905
906 EXPECT_EQ(previous, 0x0232U);
907 EXPECT_NEAR(
908 AveragePeriodUs(capture.phoenixToAuma),
909 static_cast<double>(kExpectedCycleTimeUs),
910 30.0);
911}
912
914TEST(CyclicCaptureTests, SetValveCloseContainsOneSecondCloseCommand)
915{
916 const auto capture = LoadCapture("Set valve close.pcapng");
917
918 ASSERT_EQ(capture.phoenixToAuma.size(), 552U);
919 ASSERT_EQ(capture.aumaToPhoenix.size(), 553U);
920
921 const auto transitions = CommandTransitions(capture.phoenixToAuma);
922 const std::vector<std::tuple<std::size_t, std::uint8_t, std::uint8_t>>
923 expected{{259U, kStopCommand, kCloseCommand},
924 {384U, kCloseCommand, kStopCommand}};
925 EXPECT_EQ(transitions, expected);
926
927 const auto startPacket = capture.phoenixToAuma[259U];
928 const auto stopPacket = capture.phoenixToAuma[384U];
929 const auto durationMs =
930 static_cast<double>(stopPacket.timestampNs - startPacket.timestampNs) /
931 1'000'000.0;
932
933 EXPECT_NEAR(
934 durationMs,
935 kExpectedCommandDurationMs,
936 kCommandDurationToleranceMs);
937
938 EXPECT_EQ(CommandByte(startPacket), kCloseCommand);
939 EXPECT_EQ(CommandByte(stopPacket), kStopCommand);
940
941 const auto positionTransitions = PositionTransitions(capture.aumaToPhoenix);
942 const std::vector<std::pair<std::size_t, std::uint16_t>> expectedPositions{
943 {295U, 0x022EU},
944 {332U, 0x022AU},
945 {345U, 0x0222U},
946 {371U, 0x021CU},
947 {385U, 0x0207U},
948 {398U, 0x01FFU}};
949 EXPECT_EQ(positionTransitions, expectedPositions);
950
951 std::uint16_t previous = Position(capture.aumaToPhoenix.front());
952 EXPECT_EQ(previous, 0x0231U);
953 for (std::size_t index = 1U; index < capture.aumaToPhoenix.size(); ++index)
954 {
955 const auto current = Position(capture.aumaToPhoenix[index]);
956 EXPECT_LE(current, previous);
957 previous = current;
958 }
959
960 EXPECT_EQ(previous, 0x01FFU);
961 EXPECT_NEAR(
962 AveragePeriodUs(capture.phoenixToAuma),
963 static_cast<double>(kExpectedCycleTimeUs),
964 30.0);
965}
966
968TEST(CyclicCaptureTests, CapturesUseExpectedPhoenixAndAumaMacAddresses)
969{
970 for (const auto& fileName :
971 std::array<std::string, 3>{
972 "phoenix_to_auma.pcapng",
973 "Set valve open.pcapng",
974 "Set valve close.pcapng"})
975 {
976 const auto capture = LoadCapture(fileName);
977 ASSERT_FALSE(capture.phoenixToAuma.empty());
978 ASSERT_FALSE(capture.aumaToPhoenix.empty());
979
980 for (const auto& packet : capture.phoenixToAuma)
981 {
982 EXPECT_TRUE(IsDirection(packet.frame, kPhoenixMac, kAumaMac));
983 }
984
985 for (const auto& packet : capture.aumaToPhoenix)
986 {
987 EXPECT_TRUE(IsDirection(packet.frame, kAumaMac, kPhoenixMac));
988 }
989 }
990}
991
993TEST(CyclicCaptureTests, EveryCapturedCyclicRtFrameRoundTrips)
994{
995 for (const auto& fileName :
996 std::array<std::string, 3>{
997 "phoenix_to_auma.pcapng",
998 "Set valve open.pcapng",
999 "Set valve close.pcapng"})
1000 {
1001 const auto capture = LoadCapture(fileName);
1002
1003 for (const auto& packet : capture.phoenixToAuma)
1004 {
1005 const auto parsed = ParseCapturedRtFrame(packet);
1006 const Bytes actual = parsed.ToBytes();
1007 const Bytes expected(
1008 packet.frame.begin() + 14U,
1009 packet.frame.end());
1010 EXPECT_EQ(actual, expected);
1011 }
1012
1013 for (const auto& packet : capture.phoenixToAuma)
1014 {
1015 const auto parsed = ParseCapturedRtFrame(packet);
1016 const Bytes actual = parsed.ToBytes();
1017 const Bytes expected(
1018 packet.frame.begin() + 14U,
1019 packet.frame.end());
1020 EXPECT_EQ(actual, expected);
1021 }
1022 }
1023}
1024
1026TEST(CyclicCaptureTests, AumaGsdmlMatchesCapturedProcessData)
1027{
1028 const auto auma = LoadGsdml(kAumaGsdml);
1029
1030 EXPECT_EQ(auma.vendorId, 0x013FU);
1031 EXPECT_EQ(auma.deviceId, 0x0002U);
1032 EXPECT_EQ(auma.dapModuleIdentNumber, 0x80010000U);
1033
1034 const auto slots = auma.BuildIoSlots();
1035 const auto* input = FindSlot(slots, 1U, 1U);
1036 const auto* output = FindSlot(slots, 2U, 1U);
1037
1038 ASSERT_NE(input, nullptr);
1039 ASSERT_NE(output, nullptr);
1040
1041 EXPECT_EQ(input->moduleIdent, 0x00008000U);
1042 EXPECT_EQ(input->submoduleIdent, 0x00002200U);
1043 EXPECT_EQ(input->inputLength, 34U);
1044
1045 EXPECT_EQ(output->moduleIdent, 0x00008100U);
1046 EXPECT_EQ(output->submoduleIdent, 0x10001000U);
1047 EXPECT_EQ(output->outputLength, 16U);
1048
1049 EXPECT_EQ(kInputPayloadLength, 34U + 9U);
1050 EXPECT_EQ(kOutputPayloadLength, 16U + 24U);
1051}
1052
1054TEST(CyclicCaptureTests, PhoenixGsdmlMatchesCapturedControllerIdentity)
1055{
1056 const auto phoenix = LoadGsdml(kPhoenixGsdml);
1057
1058 EXPECT_EQ(phoenix.vendorId, 0x00B0U);
1059 EXPECT_EQ(phoenix.deviceId, 0x0193U);
1060 EXPECT_EQ(phoenix.dapModuleIdentNumber, 0x00000300U);
1061 EXPECT_NE(phoenix.FindModule("PNSlot1"), nullptr);
1062}
1063
1065TEST(CyclicCaptureTests, CommandByteIsAtEthernetByteTwentyFive)
1066{
1067 const auto openCapture = LoadCapture("Set valve open.pcapng");
1068 const auto closeCapture = LoadCapture("Set valve close.pcapng");
1069
1070 ASSERT_EQ(CommandByte(openCapture.phoenixToAuma[196U]), kOpenCommand);
1071 ASSERT_EQ(CommandByte(openCapture.phoenixToAuma[321U]), kStopCommand);
1072 ASSERT_EQ(CommandByte(closeCapture.phoenixToAuma[259U]), kCloseCommand);
1073 ASSERT_EQ(CommandByte(closeCapture.phoenixToAuma[384U]), kStopCommand);
1074
1075 EXPECT_EQ(openCapture.phoenixToAuma[196U].frame.at(24U), kOpenCommand);
1076 EXPECT_EQ(openCapture.phoenixToAuma[321U].frame.at(24U), kStopCommand);
1077 EXPECT_EQ(closeCapture.phoenixToAuma[259U].frame.at(24U), kCloseCommand);
1078 EXPECT_EQ(closeCapture.phoenixToAuma[384U].frame.at(24U), kStopCommand);
1079}
1080
1082TEST(CyclicCaptureTests, AumaGsdmlResolvesCompletePhoenixAumaSlotSet)
1083{
1084 const auto auma = LoadGsdml(kAumaGsdml);
1085 const auto slots = auma.BuildIoSlots();
1086
1087 ASSERT_EQ(slots.size(), 9U);
1088
1089 const std::array<std::pair<std::uint16_t, std::uint16_t>, 9> required{
1090 {{0, 0x0001},
1091 {0, 0x0002},
1092 {0, 0x0003},
1093 {0, 0x0004},
1094 {0, 0x8000},
1095 {0, 0x8001},
1096 {0, 0x8002},
1097 {1, 1},
1098 {2, 1}}};
1099
1100 for (const auto& [slot, subslot] : required)
1101 {
1102 ASSERT_NE(FindSlot(slots, slot, subslot), nullptr);
1103 }
1104
1105 const auto* input = FindSlot(slots, 1U, 1U);
1106 const auto* output = FindSlot(slots, 2U, 1U);
1107 ASSERT_NE(input, nullptr);
1108 ASSERT_NE(output, nullptr);
1109
1110 EXPECT_EQ(input->inputLength, 34U);
1111 EXPECT_EQ(output->outputLength, 16U);
1112 EXPECT_EQ(input->moduleIdent, 0x00008000U);
1113 EXPECT_EQ(input->submoduleIdent, 0x00002200U);
1114 EXPECT_EQ(output->moduleIdent, 0x00008100U);
1115 EXPECT_EQ(output->submoduleIdent, 0x10001000U);
1116}
1117
1119TEST(CyclicCaptureTests, DISABLED_BuildIocrConfigsMatchesPhoenixAumaWireLayout)
1120{
1121 const auto slots = PhoenixAumaCapturedSlots();
1122 const auto [input, output] =
1123 rt::BuildIocrConfigs(slots, kInputFrameId, kOutputFrameId, 32, 8, 3, 43);
1124
1125 ASSERT_EQ(input.frameId, kInputFrameId);
1126 ASSERT_EQ(input.dataLength, 43U);
1127 ASSERT_EQ(input.objects.size(), 9U);
1128
1129 const auto inputSlot1 = std::find_if(
1130 input.objects.begin(),
1131 input.objects.end(),
1132 [](const rt::IODataObject& object)
1133 {
1134 return object.slot == 1U && object.subslot == 1U;
1135 });
1136 ASSERT_NE(inputSlot1, input.objects.end());
1137 EXPECT_EQ(inputSlot1->frameOffset, 1U);
1138 EXPECT_EQ(inputSlot1->dataLength, 34U);
1139 EXPECT_EQ(inputSlot1->offsetIOPS, 35U);
1140
1141 const auto inputIocs = std::find_if(
1142 input.objects.begin(),
1143 input.objects.end(),
1144 [](const rt::IODataObject& object)
1145 {
1146 return object.slot == 2U && object.subslot == 1U;
1147 });
1148 ASSERT_NE(inputIocs, input.objects.end());
1149 EXPECT_EQ(inputIocs->frameOffset, 0U);
1150 EXPECT_EQ(inputIocs->dataLength, 0U);
1151 EXPECT_EQ(inputIocs->offsetIOCS, 0U);
1152
1153 for (std::uint16_t subslot = 1U; subslot <= 4U; ++subslot)
1154 {
1155 const auto object = std::find_if(
1156 input.objects.begin(),
1157 input.objects.end(),
1158 [subslot](const rt::IODataObject& value)
1159 {
1160 return value.slot == 0U && value.subslot == subslot;
1161 });
1162 ASSERT_NE(object, input.objects.end());
1163 }
1164
1165 ASSERT_EQ(output.frameId, kOutputFrameId);
1166 ASSERT_EQ(output.dataLength, 40U);
1167 ASSERT_EQ(output.objects.size(), 9U);
1168
1169 const auto outputData = std::find_if(
1170 output.objects.begin(),
1171 output.objects.end(),
1172 [](const rt::IODataObject& object)
1173 {
1174 return object.slot == 2U && object.subslot == 1U;
1175 });
1176 ASSERT_NE(outputData, output.objects.end());
1177 EXPECT_EQ(outputData->frameOffset, 8U);
1178 EXPECT_EQ(outputData->dataLength, 16U);
1179
1180 for (std::uint16_t subslot : {1U, 2U, 3U, 4U, 0x8000U, 0x8001U, 0x8002U})
1181 {
1182 const auto object = std::find_if(
1183 output.objects.begin(),
1184 output.objects.end(),
1185 [subslot](const rt::IODataObject& value)
1186 {
1187 return value.slot == 0U && value.subslot == subslot;
1188 });
1189 ASSERT_NE(object, output.objects.end());
1190 EXPECT_EQ(object->offsetIOCS, object->frameOffset);
1191 }
1192
1193 const auto slot1Iocs = std::find_if(
1194 output.objects.begin(),
1195 output.objects.end(),
1196 [](const rt::IODataObject& object)
1197 {
1198 return object.slot == 1U && object.subslot == 1U;
1199 });
1200 ASSERT_NE(slot1Iocs, output.objects.end());
1201 EXPECT_EQ(slot1Iocs->frameOffset, 7U);
1202 EXPECT_EQ(slot1Iocs->offsetIOCS, 7U);
1203}
1204
1205TEST(CyclicCaptureTests, DISABLED_WIPRtBuildIocrConfigsMatchesPhoenixAumaWireLayout)
1206{
1207 const auto auma = LoadGsdml(kAumaGsdml);
1208
1209 const auto slots = auma.BuildIoSlots();
1210
1211 const auto [input, output] =
1212 rt::BuildIocrConfigs(slots, kInputFrameId, kOutputFrameId, 32, 8, 3, 43);
1213
1214 ASSERT_EQ(input.frameId, kInputFrameId);
1215 ASSERT_EQ(input.dataLength, 43U);
1216 ASSERT_EQ(input.objects.size(), 9U);
1217
1218 const auto inputSlot1 = std::find_if(
1219 input.objects.begin(),
1220 input.objects.end(),
1221 [](const rt::IODataObject& object)
1222 {
1223 return object.slot == 1U && object.subslot == 1U;
1224 });
1225 const auto capturedBlocks = CapturedConnectBlocks(static_cast<std::uint16_t>(BlockType::IoCrRequest));
1226 auto fullDataBlock = *FindBlock(capturedBlocks, static_cast<std::uint16_t>(BlockType::IoCrRequest));
1227
1228 auto capturedInput = PNIOCRBlockReqHeader::Parse(fullDataBlock);
1229 Bytes dataBlock(fullDataBlock.begin() + PNIOCRBlockReqHeader::kSize + 4 + 2, fullDataBlock.end()); //+4 = api nr; +2 = nr of objects
1230
1231 wire::Reader r(fullDataBlock);
1232
1234 const auto numberOfIODataObjects = r.U16();
1235
1236 r.SkipTo(PNIOCRBlockReqHeader::kSize + 6U + numberOfIODataObjects * IOCRAPIObject::kSize);
1237 const auto numberOfIOCSObjects = r.U16();
1238
1239 Bytes iocsBlock(fullDataBlock.begin() + PNIOCRBlockReqHeader::kSize + 4 + 2 + numberOfIODataObjects * IOCRAPIObject::kSize + 2, fullDataBlock.end()); //+4 = api nr; +2 = nr of objects
1240
1241 std::vector<IOCRAPIObject> objects;
1242
1243 for (std::size_t i = 0; i < numberOfIODataObjects * IOCRAPIObject::kSize; i = i + IOCRAPIObject::kSize)
1244 {
1245 const Bytes objBytes(dataBlock.begin() + i, dataBlock.begin() + i * IOCRAPIObject::kSize + IOCRAPIObject::kSize);
1246 auto obj = IOCRAPIObject::Parse(objBytes);
1247 objects.push_back(obj);
1248 }
1249 for (std::size_t i = 0; i < numberOfIOCSObjects * IOCRAPIObject::kSize; i = i + IOCRAPIObject::kSize)
1250 {
1251 const Bytes objBytes(iocsBlock.begin() + i, iocsBlock.begin() + i * IOCRAPIObject::kSize + IOCRAPIObject::kSize);
1252 auto obj = IOCRAPIObject::Parse(objBytes);
1253 objects.push_back(obj);
1254 }
1255
1256 // auto capturedInput = a;
1257 ASSERT_NE(inputSlot1, input.objects.end());
1258 EXPECT_EQ(inputSlot1->frameOffset, 1U);
1259 EXPECT_EQ(inputSlot1->frameOffset, capturedInput.frameSendOffset);
1260 EXPECT_EQ(inputSlot1->dataLength, 34U);
1261 EXPECT_EQ(inputSlot1->dataLength, capturedInput.dataLength);
1262 EXPECT_EQ(inputSlot1->offsetIOPS, 35U);
1263
1264 const auto inputIocs = std::find_if(
1265 input.objects.begin(),
1266 input.objects.end(),
1267 [](const rt::IODataObject& object)
1268 {
1269 return object.slot == 2U && object.subslot == 1U;
1270 });
1271 ASSERT_NE(inputIocs, input.objects.end());
1272 EXPECT_EQ(inputIocs->frameOffset, 0U);
1273 EXPECT_EQ(inputIocs->dataLength, 0U);
1274 EXPECT_EQ(inputIocs->offsetIOCS, 0U);
1275
1276 for (std::uint16_t subslot = 1U; subslot <= 4U; ++subslot)
1277 {
1278 const auto object = std::find_if(
1279 input.objects.begin(),
1280 input.objects.end(),
1281 [subslot](const rt::IODataObject& value)
1282 {
1283 return value.slot == 0U && value.subslot == subslot;
1284 });
1285 ASSERT_NE(object, input.objects.end());
1286 }
1287
1288 ASSERT_EQ(output.frameId, kOutputFrameId);
1289 ASSERT_EQ(output.dataLength, 40U);
1290 ASSERT_EQ(output.objects.size(), 9U);
1291
1292 const auto outputData = std::find_if(
1293 output.objects.begin(),
1294 output.objects.end(),
1295 [](const rt::IODataObject& object)
1296 {
1297 return object.slot == 2U && object.subslot == 1U;
1298 });
1299 ASSERT_NE(outputData, output.objects.end());
1300 EXPECT_EQ(outputData->frameOffset, 8U);
1301 EXPECT_EQ(outputData->dataLength, 16U);
1302
1303 for (std::uint16_t subslot : {1U, 2U, 3U, 4U, 0x8000U, 0x8001U, 0x8002U})
1304 {
1305 const auto object = std::find_if(
1306 output.objects.begin(),
1307 output.objects.end(),
1308 [subslot](const rt::IODataObject& value)
1309 {
1310 return value.slot == 0U && value.subslot == subslot;
1311 });
1312 ASSERT_NE(object, output.objects.end());
1313 EXPECT_EQ(object->offsetIOCS, object->frameOffset);
1314 }
1315
1316 const auto slot1Iocs = std::find_if(
1317 output.objects.begin(),
1318 output.objects.end(),
1319 [](const rt::IODataObject& object)
1320 {
1321 return object.slot == 1U && object.subslot == 1U;
1322 });
1323 ASSERT_NE(slot1Iocs, output.objects.end());
1324 EXPECT_EQ(slot1Iocs->frameOffset, 7U);
1325 EXPECT_EQ(slot1Iocs->offsetIOCS, 7U);
1326}
1327
1329TEST(CyclicCaptureTests, CyclicControllerTransmitsCapturedCloseCommand)
1330{
1331 auto input = CapturedInputIocr();
1332 auto output = CapturedOutputIocr();
1333 CyclicControllerCapture capture(std::move(input), std::move(output));
1334
1335 capture.SetOutputData(2, 1, Bytes{kCloseCommand});
1336 capture.Start();
1337
1338 const bool received = capture.WaitForSent(
1339 [](const Bytes& frame)
1340 {
1341 const auto parsed = rt::ParseEthernetFrame(frame);
1342 return parsed.has_value() &&
1343 parsed->frameId == kOutputFrameId &&
1344 parsed->payload.size() == kOutputPayloadLength &&
1345 parsed->payload.at(kCommandOffset) == kCloseCommand;
1346 },
1347 std::chrono::milliseconds(250));
1348
1349 capture.Stop();
1350 ASSERT_TRUE(received);
1351
1352 const auto frames = capture.SentFrames();
1353 const auto it = std::find_if(
1354 frames.begin(),
1355 frames.end(),
1356 [](const Bytes& frame)
1357 {
1358 const auto parsed = rt::ParseEthernetFrame(frame);
1359 return parsed.has_value() &&
1360 parsed->frameId == kOutputFrameId &&
1361 parsed->payload.size() == kOutputPayloadLength &&
1362 parsed->payload.at(kCommandOffset) == kCloseCommand;
1363 });
1364
1365 ASSERT_NE(it, frames.end());
1366 EXPECT_EQ(
1367 std::vector<std::uint8_t>(it->begin(), it->begin() + 6),
1368 std::vector<std::uint8_t>(kAumaMac.begin(), kAumaMac.end()));
1369 EXPECT_EQ(
1370 std::vector<std::uint8_t>(it->begin() + 6, it->begin() + 12),
1371 std::vector<std::uint8_t>(kPhoenixMac.begin(), kPhoenixMac.end()));
1372 EXPECT_EQ((*it)[12], 0x88U);
1373 EXPECT_EQ((*it)[13], 0x92U);
1374 EXPECT_EQ((*it)[14], 0x80U);
1375 EXPECT_EQ((*it)[15], 0x01U);
1376 EXPECT_EQ((*it)[24], kCloseCommand);
1377}
1378
1380TEST(CyclicCaptureTests, CyclicControllerReceivesCapturedAumaPosition)
1381{
1382 const auto capture = LoadCapture("Set valve close.pcapng");
1383 ASSERT_FALSE(capture.aumaToPhoenix.empty());
1384
1385 const auto firstInput = capture.aumaToPhoenix.front().frame;
1386 const auto expectedPosition = Position(capture.aumaToPhoenix.front());
1387 const auto expectedFrame = ParseCapturedRtFrame(capture.aumaToPhoenix.front());
1388 ASSERT_EQ(expectedFrame.frameId, kInputFrameId);
1389 ASSERT_EQ(expectedFrame.payload.size(), kInputPayloadLength);
1390
1391 CyclicControllerCapture controller{CapturedInputIocr(), CapturedOutputIocr()};
1392
1393 std::mutex callbackMutex;
1394 std::condition_variable callbackCondition;
1395 std::optional<Bytes> receivedData;
1396 controller.OnInput(
1397 [&](int slot, int subslot, const Bytes& data)
1398 {
1399 if (slot == 1 && subslot == 1)
1400 {
1401 {
1402 const std::lock_guard lock(callbackMutex);
1403 receivedData = data;
1404 }
1405 callbackCondition.notify_all();
1406 }
1407 });
1408
1409 controller.SetRxFrame(firstInput);
1410 controller.Start();
1411
1412 {
1413 std::unique_lock lock(callbackMutex);
1414 ASSERT_TRUE(callbackCondition.wait_for(
1415 lock,
1416 std::chrono::milliseconds(250),
1417 [&]
1418 {
1419 return receivedData.has_value();
1420 }));
1421 }
1422
1423 controller.Stop();
1424
1425 ASSERT_TRUE(receivedData.has_value());
1426 ASSERT_EQ(receivedData->size(), 34U);
1427
1428 const auto receivedPosition = static_cast<std::uint16_t>(
1429 (static_cast<std::uint16_t>(receivedData->at(2U)) << 8U) |
1430 receivedData->at(3U));
1431 EXPECT_EQ(receivedPosition, expectedPosition);
1432}
1433
1435TEST(CyclicCaptureTests, DISABLED_BuildInputIocrBlockMatchesPhoenixAumaCapture)
1436{
1437 auto transport = std::make_unique<StubRpcTransport>();
1438 auto* capture = transport.get();
1439 auto controlTransport = std::make_unique<StubRpcTransport>();
1440
1441 rpc::RPCCon connection(
1442 AumaRpcInfo(),
1443 {},
1444 std::move(transport),
1445 std::move(controlTransport));
1446
1447 const auto auma = LoadGsdml(kAumaGsdml);
1448
1449 rpc::IOCRSetup setup;
1450 setup.slots = auma.BuildIoSlots();
1451 setup.sendClockFactor = 32;
1452 setup.reductionRatio = 8;
1453 setup.watchdogFactor = 3;
1454 setup.dataHoldFactor = 3;
1455
1456 const auto result = connection.Connect(kPhoenixMac, true, setup);
1457 ASSERT_TRUE(result.has_value());
1458 ASSERT_EQ(capture->requests.size(), 1U);
1459
1460 const auto rpcPacket = PNRPCHeader::Parse(capture->requests.front());
1461 const auto nrd = PNNRDData::Parse(rpcPacket.payload);
1462 const auto generatedBlocks = ExtractBlocks(nrd.payload);
1463 const auto capturedBlocks = CapturedConnectBlocks(static_cast<std::uint16_t>(BlockType::IoCrRequest));
1464
1465 ASSERT_GE(generatedBlocks.size(), 3U);
1466 ASSERT_GE(capturedBlocks.size(), 3U);
1467
1468 ASSERT_NE(FindBlock(capturedBlocks, static_cast<std::uint16_t>(BlockType::IoCrRequest)), nullptr);
1469 ASSERT_NE(FindBlock(capturedBlocks, static_cast<std::uint16_t>(BlockType::IoCrRequest), 1U), nullptr);
1470
1471 EXPECT_EQ(generatedBlocks[1], *FindBlock(capturedBlocks, static_cast<std::uint16_t>(BlockType::IoCrRequest)));
1472
1473 auto parsed = PNIOCRBlockReqHeader::Parse(generatedBlocks[1]);
1474 EXPECT_EQ(parsed.iocrType, 1U);
1475 EXPECT_EQ(parsed.iocrReference, 1U);
1476 EXPECT_EQ(parsed.frameId, kInputFrameId);
1477 EXPECT_EQ(parsed.dataLength, 43U);
1478 EXPECT_EQ(parsed.sendClockFactor, 32U);
1479 EXPECT_EQ(parsed.reductionRatio, 8U);
1480 EXPECT_EQ(parsed.phase, 8U);
1481 EXPECT_EQ(parsed.watchdogFactor, 3U);
1482 EXPECT_EQ(parsed.dataHoldFactor, 3U);
1483
1484 EXPECT_EQ(generatedBlocks[2], *FindBlock(capturedBlocks, static_cast<std::uint16_t>(BlockType::IoCrRequest), 1U));
1485 parsed = PNIOCRBlockReqHeader::Parse(generatedBlocks[2]);
1486 EXPECT_EQ(parsed.iocrType, 2U);
1487 EXPECT_EQ(parsed.iocrReference, 2U);
1488 // EXPECT_EQ(parsed.frameId, kOutputFrameId);
1489 EXPECT_EQ(parsed.dataLength, 40U);
1490 EXPECT_EQ(parsed.sendClockFactor, 32U);
1491 EXPECT_EQ(parsed.reductionRatio, 8U);
1492 EXPECT_EQ(parsed.phase, 8U);
1493 EXPECT_EQ(parsed.watchdogFactor, 3U);
1494 EXPECT_EQ(parsed.dataHoldFactor, 3U);
1495}
1496
1498TEST(CyclicCaptureTests, DISABLED_BuildOutputIocrBlockMatchesPhoenixAumaCapture)
1499{
1500 auto transport = std::make_unique<StubRpcTransport>();
1501 auto* capture = transport.get();
1502 auto controlTransport = std::make_unique<StubRpcTransport>();
1503
1504 rpc::RPCCon connection(
1505 AumaRpcInfo(),
1506 {},
1507 std::move(transport),
1508 std::move(controlTransport));
1509
1510 const auto auma = LoadGsdml(kAumaGsdml);
1511
1512 rpc::IOCRSetup setup;
1513 setup.slots = auma.BuildIoSlots();
1514 setup.sendClockFactor = 32;
1515 setup.reductionRatio = 8;
1516 setup.watchdogFactor = 3;
1517 setup.dataHoldFactor = 3;
1518
1519 const auto result = connection.Connect(kPhoenixMac, true, setup);
1520 ASSERT_TRUE(result.has_value());
1521 ASSERT_EQ(capture->requests.size(), 1U);
1522
1523 const auto rpcPacket = PNRPCHeader::Parse(capture->requests.front());
1524 const auto nrd = PNNRDData::Parse(rpcPacket.payload);
1525 const auto generatedBlocks = ExtractBlocks(nrd.payload);
1526 const auto capturedBlocks = CapturedConnectBlocks(static_cast<std::uint16_t>(BlockType::IoCrRequest));
1527
1528 ASSERT_GE(generatedBlocks.size(), 3U);
1529
1530 const auto* capturedOutput =
1531 FindBlock(capturedBlocks, static_cast<std::uint16_t>(BlockType::IoCrRequest), 1U);
1532 ASSERT_NE(capturedOutput, nullptr);
1533 EXPECT_EQ(generatedBlocks[2], *capturedOutput);
1534
1535 const auto parsed = PNIOCRBlockReqHeader::Parse(generatedBlocks[2]);
1536 EXPECT_EQ(parsed.iocrType, 2U);
1537 EXPECT_EQ(parsed.iocrReference, 2U);
1538 EXPECT_EQ(parsed.frameId, 0xC001U);
1539 EXPECT_EQ(parsed.dataLength, 40U);
1540 EXPECT_EQ(parsed.sendClockFactor, 32U);
1541 EXPECT_EQ(parsed.reductionRatio, 8U);
1542 EXPECT_EQ(parsed.phase, 8U);
1543 EXPECT_EQ(parsed.watchdogFactor, 3U);
1544 EXPECT_EQ(parsed.dataHoldFactor, 3U);
1545}
1546
1548TEST(CyclicCaptureTests, BuildExpectedSubmoduleBlocksMatchPhoenixAumaCapture)
1549{
1550 auto transport = std::make_unique<StubRpcTransport>();
1551 auto* capture = transport.get();
1552 auto controlTransport = std::make_unique<StubRpcTransport>();
1553
1554 rpc::RPCCon connection(
1555 AumaRpcInfo(),
1556 {},
1557 std::move(transport),
1558 std::move(controlTransport));
1559
1560 const auto auma = LoadGsdml(kAumaGsdml);
1561
1562 rpc::IOCRSetup setup;
1563 setup.slots = auma.BuildIoSlots();
1564 setup.sendClockFactor = 32;
1565 setup.reductionRatio = 8;
1566 setup.watchdogFactor = 3;
1567 setup.dataHoldFactor = 3;
1568
1569 const auto result = connection.Connect(kPhoenixMac, true, setup);
1570 ASSERT_TRUE(result.has_value());
1571 ASSERT_EQ(capture->requests.size(), 1U);
1572
1573 const auto rpcPacket = PNRPCHeader::Parse(capture->requests.front());
1574 const auto nrd = PNNRDData::Parse(rpcPacket.payload);
1575 const auto generatedBlocks = ExtractBlocks(nrd.payload);
1576 const auto capturedBlocks = CapturedConnectBlocks(static_cast<std::uint16_t>(BlockType::ExpectedSubmoduleRequest));
1577
1578 std::vector<Bytes> generatedExpected;
1579 std::vector<Bytes> capturedExpected;
1580
1581 for (const auto& block : generatedBlocks)
1582 {
1583 if (rpc::PeekBlockHeader(block, 0U).blockType ==
1584 static_cast<std::uint16_t>(BlockType::ExpectedSubmoduleRequest))
1585 {
1586 generatedExpected.push_back(block);
1587 }
1588 }
1589
1590 for (const auto& block : capturedBlocks)
1591 {
1592 if (rpc::PeekBlockHeader(block, 0U).blockType ==
1593 static_cast<std::uint16_t>(BlockType::ExpectedSubmoduleRequest))
1594 {
1595 capturedExpected.push_back(block);
1596 }
1597 }
1598
1599 ASSERT_EQ(capturedExpected.size(), 3U);
1600 EXPECT_EQ(generatedExpected, capturedExpected);
1601}
1602
1603} // namespace profinet::CyclicCaptureTests
GoogleMock implementation of IRawEthernetSocket.
Connection to a PROFINET IO-Device: establishes an AR (Application Relationship) and performs acyclic...
Definition rpc.h:138
Lightweight cursor for parsing a big-endian byte buffer without copies.
Definition wire.h:192
void SkipTo(std::size_t offset)
Move the read position to an absolute byte offset.
Definition wire.h:343
std::uint16_t U16()
Read and consume a big-endian 16-bit value.
Definition wire.h:258
Declares the PROFINET RTC1 cyclic IO controller and process-data exchange API.
xmlXPathObjectPtr obj
Definition gsdml.cpp:82
Declares the GSDML parser and PROFINET device-description data model.
GoogleMock implementation of IRawEthernetSocket.
TEST(CyclicCaptureTests, PhoenixToAumaCaptureHasExpectedCyclicContract)
GSDMLDevice ParseGsdml(const std::string &path)
Parse a GSDML file into a GSDMLDevice.
Definition gsdml.cpp:1398
BlockHeaderView PeekBlockHeader(const Bytes &data, std::size_t offset)
Definition rpc.h:54
std::optional< RTFrame > ParseEthernetFrame(const Bytes &data)
Parse a complete Ethernet frame and extract its RT frame.
Definition rt.cpp:261
constexpr std::uint16_t IOCR_TYPE_INPUT
IOCR direction: Device -> Controller.
Definition rt.h:66
std::pair< IOCRConfig, IOCRConfig > BuildIocrConfigs(const std::vector< SlotT > &slots, std::uint16_t inputFrameId, std::uint16_t outputFrameId, std::uint16_t sendClockFactor=32, std::uint16_t reductionRatio=32, int watchdogFactor=3, std::uint16_t cSduLength=40)
Definition rt.h:454
constexpr std::uint16_t ETHERTYPE_PROFINET
EtherType used by PROFINET RT frames (0x8892).
Definition rt.h:34
constexpr std::uint16_t IOCR_TYPE_OUTPUT
IOCR direction: Controller -> Device.
Definition rt.h:68
@ IoCrRequest
IOCR block request.
@ ApplicationRelationRequest
AR block request.
@ ExpectedSubmoduleRequest
Expected submodule block request.
constexpr int blockHeaderLength
Constant lenght of a blockHeader.
Definition protocol.h:58
std::filesystem::path GetExecutableDirectory()
Retrieves the absolute directory path of the currently running executable.
constexpr std::uint16_t kInputFrameId
std::vector< std::uint8_t > Bytes
Generic byte buffer alias used throughout the library for raw wire data.
Definition protocol.h:55
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::size_t kSize
Size in bytes of this object.
Definition protocol.h:1907
static IOCRAPIObject Parse(const Bytes &data)
Parse an IOCR API object from raw bytes.
Definition protocol.h:1912
static constexpr std::size_t kSize
Size in bytes of this header.
Definition protocol.h:2042
static PNIOCRBlockReqHeader Parse(const Bytes &data)
Parse the fixed IOCRBlockReq header.
Definition protocol.h:2048
static PNNRDData Parse(const Bytes &data)
Parse an NRD wrapper and payload from raw bytes.
Definition protocol.h:1153
static PNRPCHeader Parse(const Bytes &data)
Parse an RPC header and body from raw bytes.
Definition protocol.h:1012
std::uint16_t blockType
Definition rpc.h:50
Configuration for establishing cyclic IO alongside an AR.
Definition rpcTypes.h:149
std::uint16_t watchdogFactor
Watchdog factor (missed-frame tolerance before a timeout fault).
Definition rpcTypes.h:160
std::uint16_t dataHoldFactor
Data hold factor (how long to keep the last good frame's data).
Definition rpcTypes.h:163
std::vector< IOSlot > slots
Slots to include in the cyclic data frames.
Definition rpcTypes.h:151
std::uint16_t reductionRatio
Reduction ratio relative to the send clock.
Definition rpcTypes.h:157
std::uint16_t sendClockFactor
Send clock base factor (31.25us units).
Definition rpcTypes.h:154
Single IO data object within the C_SDU: one piece of process data mapped to a slot/subslot,...
Definition rt.h:109
std::uint16_t frameOffset
Byte offset of this object's data within the cyclic frame.
Definition rt.h:117
std::optional< std::uint16_t > offsetIOCS
Byte offset of this object's IOCS (consumer status) byte, or 0 if not applicable.
Definition rt.h:126
Stub implementation of IRpcTransport.
std::mutex mutex
std::uint64_t timestampNs
std::vector< Bytes > sentFrames
Bytes rxFrame
std::condition_variable condition
std::unique_ptr< MockRawEthernetSocket > tx
std::unique_ptr< MockRawEthernetSocket > rx
std::vector< CapturedPacket > phoenixToAuma
std::vector< CapturedPacket > aumaToPhoenix
std::unique_ptr< cyclic::CyclicController > controller
Bytes frame
std::unique_ptr< asio::io_context > ioContext
Shared ASIO context.
MockRawEthernetSocket * txSocket
MockRawEthernetSocket * rxSocket
Bytes payload
Definition test_dcp.cpp:392
Declares Linux Ethernet, addressing, timing, and utility helpers used by the PROFINET stack.