55using Mac = std::array<std::uint8_t, 6>;
57constexpr Mac kPhoenixMac{0xA8, 0x74, 0x1D, 0x44, 0x7A, 0xBB};
58constexpr Mac kAumaMac{0xA0, 0xD3, 0x85, 0x00, 0xF6, 0x13};
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;
67constexpr std::uint8_t kStopCommand = 0x00;
68constexpr std::uint8_t kOpenCommand = 0x01;
69constexpr std::uint8_t kCloseCommand = 0x02;
71constexpr std::uint8_t kOutputDataStatus = 0x35;
72constexpr std::uint8_t kInputDataStatus = 0x15;
74constexpr std::uint32_t kExpectedCycleTimeUs = 8000U;
75constexpr double kExpectedCommandDurationMs = 1000.0;
76constexpr double kCommandDurationToleranceMs = 5.0;
78constexpr char kAumaGsdml[] =
"GSDML-V2.42-AUMA-SGX-20220920.xml";
79constexpr char kPhoenixGsdml[] =
80 "GSDML-V2.45-Phoenix Contact-AXC F 1252-20250813.xml";
99 if (offset + 4U > bytes.size())
101 throw std::runtime_error(
"PCAPNG u32 read out of range");
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);
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]);
119std::uint64_t TimestampToNs(
121 std::uint8_t resolution)
123 if ((resolution & 0x80U) == 0U)
125 const auto exponent =
static_cast<unsigned>(resolution);
126 std::uint64_t divisor = 1U;
127 for (
unsigned index = 0U; index < exponent; ++index)
132 return static_cast<std::uint64_t
>(
133 static_cast<long double>(ticks) * 1'000'000'000.0L /
134 static_cast<long double>(divisor));
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));
144std::filesystem::path CapturePath(
const std::string& fileName)
146 const std::array<std::filesystem::path, 5> candidates{
149 std::filesystem::current_path() /
"tests" /
"captures" / fileName,
150 std::filesystem::current_path() /
"captures" / fileName,
151 std::filesystem::current_path() / fileName};
153 for (
const auto& candidate : candidates)
155 if (std::filesystem::exists(candidate))
161 ADD_FAILURE() <<
"PCAPNG fixture not found: " << fileName;
166std::vector<CapturedPacket> ReadPcapng(
const std::filesystem::path& path)
168 std::ifstream file(path, std::ios::binary);
171 throw std::runtime_error(
"Cannot open PCAPNG fixture: " + path.string());
175 std::istreambuf_iterator<char>(file),
176 std::istreambuf_iterator<char>()};
178 if (bytes.size() < 12U)
180 throw std::runtime_error(
"PCAPNG fixture is too short: " + path.string());
183 const auto byteOrderMagic = ReadU32(bytes, 8U,
true);
184 const bool littleEndian = byteOrderMagic == 0x1A2B3C4DU;
185 if (!littleEndian && byteOrderMagic != 0x4D3C2B1AU)
187 throw std::runtime_error(
"Unsupported PCAPNG byte-order magic");
190 std::vector<std::uint8_t> timestampResolution{6U};
191 std::vector<CapturedPacket> packets;
193 std::size_t offset = 0U;
194 while (offset + 12U <= bytes.size())
196 const auto blockType = ReadU32(bytes, offset, littleEndian);
197 const auto blockLength = ReadU32(bytes, offset + 4U, littleEndian);
199 if (blockLength < 12U || offset + blockLength > bytes.size())
201 throw std::runtime_error(
"Invalid PCAPNG block length");
204 if (blockType == 0x00000001U && blockLength >= 20U)
207 std::size_t optionOffset = offset + 16U;
208 const std::size_t optionEnd = offset + blockLength - 4U;
210 while (optionOffset + 4U <= optionEnd)
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);
223 if (optionOffset + optionLength > optionEnd)
228 if (code == 9U && optionLength >= 1U)
230 timestampResolution[0] = bytes[optionOffset];
233 optionOffset += ((optionLength + 3U) / 4U) * 4U;
236 else if (blockType == 0x00000006U && blockLength >= 32U)
238 const auto interfaceId = ReadU32(bytes, offset + 8U, littleEndian);
239 if (interfaceId >= timestampResolution.size())
241 throw std::runtime_error(
"PCAPNG interface id out of range");
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);
248 const auto packetOffset = offset + 28U;
249 if (packetOffset + capturedLength > offset + blockLength - 4U)
251 throw std::runtime_error(
"PCAPNG packet exceeds its block");
255 (
static_cast<std::uint64_t
>(timestampHigh) << 32U) |
256 static_cast<std::uint64_t
>(timestampLow);
258 CapturedPacket packet;
260 TimestampToNs(ticks, timestampResolution[interfaceId]);
262 bytes.begin() + packetOffset,
264 packetOffset + capturedLength);
265 packets.push_back(std::move(packet));
268 offset += blockLength;
278 const Mac& destination)
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 &&
288Capture LoadCapture(
const std::string& fileName)
290 const auto packets = ReadPcapng(CapturePath(fileName));
293 for (
const auto& packet : packets)
295 if (IsDirection(packet.frame, kPhoenixMac, kAumaMac))
297 capture.phoenixToAuma.push_back(packet);
299 else if (IsDirection(packet.frame, kAumaMac, kPhoenixMac))
301 capture.aumaToPhoenix.push_back(packet);
309rt::RTFrame ParseCapturedRtFrame(
const CapturedPacket& packet)
312 if (!parsed.has_value())
314 throw std::runtime_error(
"Captured frame is not a valid PROFINET RT frame");
320std::uint8_t CommandByte(
const CapturedPacket& packet)
322 return ParseCapturedRtFrame(packet).payload.at(kCommandOffset);
326std::uint16_t Position(
const CapturedPacket& packet)
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)));
335std::vector<std::tuple<std::size_t, std::uint8_t, std::uint8_t>> CommandTransitions(
336 const std::vector<CapturedPacket>& packets)
338 std::vector<std::tuple<std::size_t, std::uint8_t, std::uint8_t>> transitions;
344 auto previous = CommandByte(packets.front());
345 for (std::size_t index = 1U; index < packets.size(); ++index)
347 const auto current = CommandByte(packets[index]);
348 if (current != previous)
350 transitions.emplace_back(index, previous, current);
358std::vector<std::pair<std::size_t, std::uint16_t>> PositionTransitions(
359 const std::vector<CapturedPacket>& packets)
361 std::vector<std::pair<std::size_t, std::uint16_t>> transitions;
367 auto previous = Position(packets.front());
368 for (std::size_t index = 1U; index < packets.size(); ++index)
370 const auto current = Position(packets[index]);
371 if (current != previous)
373 transitions.emplace_back(index, current);
381double AveragePeriodUs(
const std::vector<CapturedPacket>& packets)
383 if (packets.size() < 2U)
388 std::uint64_t sumNs = 0U;
389 for (std::size_t index = 1U; index < packets.size(); ++index)
391 sumNs += packets[index].timestampNs - packets[index - 1U].timestampNs;
394 return static_cast<double>(sumNs) /
395 static_cast<double>(packets.size() - 1U) /
400gsdml::GSDMLDevice LoadGsdml(
const std::string& fileName)
402 const std::array<std::filesystem::path, 4> candidates{
405 std::filesystem::current_path() /
"tests" /
"gsdml" / fileName,
406 std::filesystem::current_path() /
"gsdml" / fileName};
408 for (
const auto& candidate : candidates)
410 if (std::filesystem::exists(candidate))
416 ADD_FAILURE() <<
"GSDML fixture not found: " << fileName;
421const rpc::IOSlot* FindSlot(
422 const std::vector<rpc::IOSlot>& slots,
424 std::uint16_t subslot)
426 const auto it = std::find_if(
429 [slot, subslot](
const rpc::IOSlot& value)
431 return value.slot == slot && value.subslot == subslot;
434 return it == slots.end() ? nullptr : &(*it);
444std::vector<rpc::IOSlot> PhoenixAumaCapturedSlots()
446 std::vector<rpc::IOSlot> slots;
448 const auto addDap = [&slots](
449 std::uint16_t subslot,
450 std::uint32_t moduleIdent,
451 std::uint32_t submoduleIdent)
455 slot.subslot = subslot;
456 slot.moduleIdent = moduleIdent;
457 slot.submoduleIdent = submoduleIdent;
458 slots.push_back(slot);
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);
474 input.inputLength = 34;
475 input.moduleIdent = 0x00008000U;
476 input.submoduleIdent = 0x00002200U;
477 slots.push_back(input);
482 output.outputLength = 16;
483 output.moduleIdent = 0x00008100U;
484 output.submoduleIdent = 0x10001000U;
485 slots.push_back(output);
491rt::IOCRConfig CapturedInputIocr()
493 rt::IOCRConfig config;
495 config.referenceIOCR = 1;
497 config.sendClockFactor = 32;
498 config.reductionRatio = 8;
500 config.watchdogFactor = 3;
501 config.dataLength = 43;
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},
512 config.objects.back().offsetIOCS = 0;
517rt::IOCRConfig CapturedOutputIocr()
519 rt::IOCRConfig config;
521 config.referenceIOCR = 2;
523 config.sendClockFactor = 32;
524 config.reductionRatio = 8;
526 config.watchdogFactor = 3;
527 config.dataLength = 40;
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},
537 {2, 1, 8, 16, 24, 0}};
544 std::vector<Bytes> result;
545 std::size_t offset = 0U;
549 const std::size_t blockSize = 4U + header.blockLength;
550 if (blockSize >
payload.size() - offset)
557 payload.begin() + offset + blockSize);
564std::vector<Bytes> CapturedConnectBlocks(std::uint16_t captureThisBlock)
566 const auto packets = ReadPcapng(CapturePath(
"phoenix_to_auma.pcapng"));
567 std::vector<Bytes> blocks;
569 for (
const auto& packet : packets)
572 const Bytes marker{0x01, 0x01, 0x00, 0x43};
573 const auto markerIt = std::search(
574 packet.frame.begin(),
579 if (markerIt == packet.frame.end())
584 const std::size_t start =
static_cast<std::size_t
>(
585 std::distance(packet.frame.begin(), markerIt));
587 std::size_t offset = start;
591 const std::size_t blockSize = 4U + header.blockLength;
593 if (blockSize > packet.frame.size() - offset)
601 packet.frame.begin() + offset,
602 packet.frame.begin() +
605 if (header.blockType == captureThisBlock)
608 packet.frame.begin() + offset,
609 packet.frame.begin() +
619 ADD_FAILURE() <<
"CONNECT request blocks were not found in phoenix_to_auma.pcapng";
624const Bytes* FindBlock(
625 const std::vector<Bytes>& blocks,
626 std::uint16_t blockType,
627 std::size_t occurrence = 0U)
629 std::size_t count = 0U;
630 for (
const auto& block : blocks)
634 if (count == occurrence)
645dcp::DCPDeviceDescription AumaRpcInfo()
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;
658class CyclicControllerCapture
662 explicit CyclicControllerCapture(
663 rt::IOCRConfig input,
664 rt::IOCRConfig output)
666 tx = std::make_unique<testing::NiceMock<MockRawEthernetSocket>>();
667 rx = std::make_unique<testing::NiceMock<MockRawEthernetSocket>>();
672 ON_CALL(*txSocket, Send(testing::_))
676 const std::lock_guard lock(mutex);
682 ON_CALL(*rxSocket, Recv())
683 .WillByDefault([
this]
685 const std::lock_guard lock(mutex);
689 ON_CALL(*rxSocket, SetTimeout(testing::_))
690 .WillByDefault([](std::chrono::milliseconds) {});
691 ioContext = std::make_unique<asio::io_context>();
693 controller = std::make_unique<cyclic::CyclicController>(
707 const std::lock_guard lock(mutex);
714 controller->Start(std::move(tx), std::move(rx));
729 controller->SetOutputData(slot, subslot, data);
733 void OnInput(std::function<
void(
int,
int,
const Bytes&)> callback)
739 template <
typename Predicate>
740 bool WaitForSent(Predicate&& predicate, std::chrono::milliseconds timeout)
742 std::unique_lock lock(mutex);
748 return std::ranges::any_of(sentFrames, predicate);
753 std::vector<Bytes> SentFrames()
const
755 const std::lock_guard lock(mutex);
762 std::unique_ptr<MockRawEthernetSocket>
tx;
763 std::unique_ptr<MockRawEthernetSocket>
rx;
780TEST(CyclicCaptureTests, PhoenixToAumaCaptureHasExpectedCyclicContract)
782 const auto capture = LoadCapture(
"phoenix_to_auma.pcapng");
784 ASSERT_EQ(capture.phoenixToAuma.size(), 399U);
785 ASSERT_EQ(capture.aumaToPhoenix.size(), 399U);
787 const auto phoenixRt = std::count_if(
788 capture.phoenixToAuma.begin(),
789 capture.phoenixToAuma.end(),
790 [](
const CapturedPacket& packet)
792 return packet.frame.size() >= 16U &&
793 ParseCapturedRtFrame(packet).frameId == kOutputFrameId;
796 const auto aumaRt = std::count_if(
797 capture.aumaToPhoenix.begin(),
798 capture.aumaToPhoenix.end(),
799 [](
const CapturedPacket& packet)
801 return packet.frame.size() >= 16U &&
802 ParseCapturedRtFrame(packet).frameId == kInputFrameId;
805 EXPECT_EQ(phoenixRt, 399);
806 EXPECT_EQ(aumaRt, 398);
808 const auto firstAumaFrame = ParseCapturedRtFrame(capture.aumaToPhoenix.front());
809 EXPECT_EQ(firstAumaFrame.frameId, 0xFEFF);
810 EXPECT_EQ(firstAumaFrame.payload.size(), 102U);
812 for (
const auto& packet : capture.phoenixToAuma)
814 const auto frame = ParseCapturedRtFrame(packet);
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);
822 for (std::size_t index = 1U; index < capture.aumaToPhoenix.size(); ++index)
824 const auto frame = ParseCapturedRtFrame(capture.aumaToPhoenix[index]);
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);
833 AveragePeriodUs(capture.phoenixToAuma),
839TEST(CyclicCaptureTests, SetValveOpenContainsOneSecondOpenCommand)
841 const auto capture = LoadCapture(
"Set valve open.pcapng");
843 ASSERT_EQ(capture.aumaToPhoenix.size(), 626U);
844 ASSERT_EQ(capture.phoenixToAuma.size(), 627U);
846 for (
const auto& packet : capture.phoenixToAuma)
848 const auto frame = ParseCapturedRtFrame(packet);
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);
856 for (
const auto& packet : capture.aumaToPhoenix)
858 const auto frame = ParseCapturedRtFrame(packet);
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);
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);
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) /
880 kExpectedCommandDurationMs,
881 kCommandDurationToleranceMs);
883 EXPECT_EQ(CommandByte(startPacket), kOpenCommand);
884 EXPECT_EQ(CommandByte(stopPacket), kStopCommand);
886 const auto positionTransitions = PositionTransitions(capture.aumaToPhoenix);
887 const std::vector<std::pair<std::size_t, std::uint16_t>> expectedPositions{
895 EXPECT_EQ(positionTransitions, expectedPositions);
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)
901 const auto current = Position(capture.aumaToPhoenix[index]);
902 EXPECT_GE(current, previous);
906 EXPECT_EQ(previous, 0x0232U);
908 AveragePeriodUs(capture.phoenixToAuma),
909 static_cast<double>(kExpectedCycleTimeUs),
914TEST(CyclicCaptureTests, SetValveCloseContainsOneSecondCloseCommand)
916 const auto capture = LoadCapture(
"Set valve close.pcapng");
918 ASSERT_EQ(capture.phoenixToAuma.size(), 552U);
919 ASSERT_EQ(capture.aumaToPhoenix.size(), 553U);
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);
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) /
935 kExpectedCommandDurationMs,
936 kCommandDurationToleranceMs);
938 EXPECT_EQ(CommandByte(startPacket), kCloseCommand);
939 EXPECT_EQ(CommandByte(stopPacket), kStopCommand);
941 const auto positionTransitions = PositionTransitions(capture.aumaToPhoenix);
942 const std::vector<std::pair<std::size_t, std::uint16_t>> expectedPositions{
949 EXPECT_EQ(positionTransitions, expectedPositions);
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)
955 const auto current = Position(capture.aumaToPhoenix[index]);
956 EXPECT_LE(current, previous);
960 EXPECT_EQ(previous, 0x01FFU);
962 AveragePeriodUs(capture.phoenixToAuma),
963 static_cast<double>(kExpectedCycleTimeUs),
968TEST(CyclicCaptureTests, CapturesUseExpectedPhoenixAndAumaMacAddresses)
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"})
976 const auto capture = LoadCapture(fileName);
977 ASSERT_FALSE(capture.phoenixToAuma.empty());
978 ASSERT_FALSE(capture.aumaToPhoenix.empty());
980 for (
const auto& packet : capture.phoenixToAuma)
982 EXPECT_TRUE(IsDirection(packet.frame, kPhoenixMac, kAumaMac));
985 for (
const auto& packet : capture.aumaToPhoenix)
987 EXPECT_TRUE(IsDirection(packet.frame, kAumaMac, kPhoenixMac));
993TEST(CyclicCaptureTests, EveryCapturedCyclicRtFrameRoundTrips)
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"})
1001 const auto capture = LoadCapture(fileName);
1003 for (
const auto& packet : capture.phoenixToAuma)
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);
1013 for (
const auto& packet : capture.phoenixToAuma)
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);
1026TEST(CyclicCaptureTests, AumaGsdmlMatchesCapturedProcessData)
1028 const auto auma = LoadGsdml(kAumaGsdml);
1030 EXPECT_EQ(auma.vendorId, 0x013FU);
1031 EXPECT_EQ(auma.deviceId, 0x0002U);
1032 EXPECT_EQ(auma.dapModuleIdentNumber, 0x80010000U);
1034 const auto slots = auma.BuildIoSlots();
1035 const auto* input = FindSlot(slots, 1U, 1U);
1036 const auto* output = FindSlot(slots, 2U, 1U);
1038 ASSERT_NE(input,
nullptr);
1039 ASSERT_NE(output,
nullptr);
1041 EXPECT_EQ(input->moduleIdent, 0x00008000U);
1042 EXPECT_EQ(input->submoduleIdent, 0x00002200U);
1043 EXPECT_EQ(input->inputLength, 34U);
1045 EXPECT_EQ(output->moduleIdent, 0x00008100U);
1046 EXPECT_EQ(output->submoduleIdent, 0x10001000U);
1047 EXPECT_EQ(output->outputLength, 16U);
1049 EXPECT_EQ(kInputPayloadLength, 34U + 9U);
1050 EXPECT_EQ(kOutputPayloadLength, 16U + 24U);
1054TEST(CyclicCaptureTests, PhoenixGsdmlMatchesCapturedControllerIdentity)
1056 const auto phoenix = LoadGsdml(kPhoenixGsdml);
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);
1065TEST(CyclicCaptureTests, CommandByteIsAtEthernetByteTwentyFive)
1067 const auto openCapture = LoadCapture(
"Set valve open.pcapng");
1068 const auto closeCapture = LoadCapture(
"Set valve close.pcapng");
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);
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);
1082TEST(CyclicCaptureTests, AumaGsdmlResolvesCompletePhoenixAumaSlotSet)
1084 const auto auma = LoadGsdml(kAumaGsdml);
1085 const auto slots = auma.BuildIoSlots();
1087 ASSERT_EQ(slots.size(), 9U);
1089 const std::array<std::pair<std::uint16_t, std::uint16_t>, 9> required{
1100 for (
const auto& [slot, subslot] : required)
1102 ASSERT_NE(FindSlot(slots, slot, subslot),
nullptr);
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);
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);
1119TEST(CyclicCaptureTests, DISABLED_BuildIocrConfigsMatchesPhoenixAumaWireLayout)
1121 const auto slots = PhoenixAumaCapturedSlots();
1122 const auto [input, output] =
1126 ASSERT_EQ(input.dataLength, 43U);
1127 ASSERT_EQ(input.objects.size(), 9U);
1129 const auto inputSlot1 = std::find_if(
1130 input.objects.begin(),
1131 input.objects.end(),
1134 return object.slot == 1U && object.subslot == 1U;
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);
1141 const auto inputIocs = std::find_if(
1142 input.objects.begin(),
1143 input.objects.end(),
1146 return object.slot == 2U && object.subslot == 1U;
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);
1153 for (std::uint16_t subslot = 1U; subslot <= 4U; ++subslot)
1155 const auto object = std::find_if(
1156 input.objects.begin(),
1157 input.objects.end(),
1160 return value.slot == 0U && value.subslot == subslot;
1162 ASSERT_NE(
object, input.objects.end());
1166 ASSERT_EQ(output.dataLength, 40U);
1167 ASSERT_EQ(output.objects.size(), 9U);
1169 const auto outputData = std::find_if(
1170 output.objects.begin(),
1171 output.objects.end(),
1174 return object.slot == 2U && object.subslot == 1U;
1176 ASSERT_NE(outputData, output.objects.end());
1177 EXPECT_EQ(outputData->frameOffset, 8U);
1178 EXPECT_EQ(outputData->dataLength, 16U);
1180 for (std::uint16_t subslot : {1U, 2U, 3U, 4U, 0x8000U, 0x8001U, 0x8002U})
1182 const auto object = std::find_if(
1183 output.objects.begin(),
1184 output.objects.end(),
1187 return value.slot == 0U && value.subslot == subslot;
1189 ASSERT_NE(
object, output.objects.end());
1193 const auto slot1Iocs = std::find_if(
1194 output.objects.begin(),
1195 output.objects.end(),
1198 return object.slot == 1U && object.subslot == 1U;
1200 ASSERT_NE(slot1Iocs, output.objects.end());
1201 EXPECT_EQ(slot1Iocs->frameOffset, 7U);
1202 EXPECT_EQ(slot1Iocs->offsetIOCS, 7U);
1205TEST(CyclicCaptureTests, DISABLED_WIPRtBuildIocrConfigsMatchesPhoenixAumaWireLayout)
1207 const auto auma = LoadGsdml(kAumaGsdml);
1209 const auto slots = auma.BuildIoSlots();
1211 const auto [input, output] =
1215 ASSERT_EQ(input.dataLength, 43U);
1216 ASSERT_EQ(input.objects.size(), 9U);
1218 const auto inputSlot1 = std::find_if(
1219 input.objects.begin(),
1220 input.objects.end(),
1223 return object.slot == 1U && object.subslot == 1U;
1234 const auto numberOfIODataObjects = r.
U16();
1237 const auto numberOfIOCSObjects = r.
U16();
1241 std::vector<IOCRAPIObject> objects;
1247 objects.push_back(
obj);
1253 objects.push_back(
obj);
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);
1264 const auto inputIocs = std::find_if(
1265 input.objects.begin(),
1266 input.objects.end(),
1269 return object.slot == 2U && object.subslot == 1U;
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);
1276 for (std::uint16_t subslot = 1U; subslot <= 4U; ++subslot)
1278 const auto object = std::find_if(
1279 input.objects.begin(),
1280 input.objects.end(),
1283 return value.slot == 0U && value.subslot == subslot;
1285 ASSERT_NE(
object, input.objects.end());
1289 ASSERT_EQ(output.dataLength, 40U);
1290 ASSERT_EQ(output.objects.size(), 9U);
1292 const auto outputData = std::find_if(
1293 output.objects.begin(),
1294 output.objects.end(),
1297 return object.slot == 2U && object.subslot == 1U;
1299 ASSERT_NE(outputData, output.objects.end());
1300 EXPECT_EQ(outputData->frameOffset, 8U);
1301 EXPECT_EQ(outputData->dataLength, 16U);
1303 for (std::uint16_t subslot : {1U, 2U, 3U, 4U, 0x8000U, 0x8001U, 0x8002U})
1305 const auto object = std::find_if(
1306 output.objects.begin(),
1307 output.objects.end(),
1310 return value.slot == 0U && value.subslot == subslot;
1312 ASSERT_NE(
object, output.objects.end());
1316 const auto slot1Iocs = std::find_if(
1317 output.objects.begin(),
1318 output.objects.end(),
1321 return object.slot == 1U && object.subslot == 1U;
1323 ASSERT_NE(slot1Iocs, output.objects.end());
1324 EXPECT_EQ(slot1Iocs->frameOffset, 7U);
1325 EXPECT_EQ(slot1Iocs->offsetIOCS, 7U);
1329TEST(CyclicCaptureTests, CyclicControllerTransmitsCapturedCloseCommand)
1331 auto input = CapturedInputIocr();
1332 auto output = CapturedOutputIocr();
1333 CyclicControllerCapture capture(std::move(input), std::move(output));
1335 capture.SetOutputData(2, 1,
Bytes{kCloseCommand});
1338 const bool received = capture.WaitForSent(
1342 return parsed.has_value() &&
1344 parsed->payload.size() == kOutputPayloadLength &&
1345 parsed->payload.at(kCommandOffset) == kCloseCommand;
1347 std::chrono::milliseconds(250));
1350 ASSERT_TRUE(received);
1352 const auto frames = capture.SentFrames();
1353 const auto it = std::find_if(
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;
1365 ASSERT_NE(it, frames.end());
1367 std::vector<std::uint8_t>(it->begin(), it->begin() + 6),
1368 std::vector<std::uint8_t>(kAumaMac.begin(), kAumaMac.end()));
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);
1380TEST(CyclicCaptureTests, CyclicControllerReceivesCapturedAumaPosition)
1382 const auto capture = LoadCapture(
"Set valve close.pcapng");
1383 ASSERT_FALSE(capture.aumaToPhoenix.empty());
1385 const auto firstInput = capture.aumaToPhoenix.front().frame;
1386 const auto expectedPosition = Position(capture.aumaToPhoenix.front());
1387 const auto expectedFrame = ParseCapturedRtFrame(capture.aumaToPhoenix.front());
1389 ASSERT_EQ(expectedFrame.payload.size(), kInputPayloadLength);
1391 CyclicControllerCapture
controller{CapturedInputIocr(), CapturedOutputIocr()};
1393 std::mutex callbackMutex;
1394 std::condition_variable callbackCondition;
1395 std::optional<Bytes> receivedData;
1397 [&](
int slot,
int subslot,
const Bytes& data)
1399 if (slot == 1 && subslot == 1)
1402 const std::lock_guard lock(callbackMutex);
1403 receivedData = data;
1405 callbackCondition.notify_all();
1413 std::unique_lock lock(callbackMutex);
1414 ASSERT_TRUE(callbackCondition.wait_for(
1416 std::chrono::milliseconds(250),
1419 return receivedData.has_value();
1425 ASSERT_TRUE(receivedData.has_value());
1426 ASSERT_EQ(receivedData->size(), 34U);
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);
1435TEST(CyclicCaptureTests, DISABLED_BuildInputIocrBlockMatchesPhoenixAumaCapture)
1437 auto transport = std::make_unique<StubRpcTransport>();
1438 auto* capture = transport.get();
1439 auto controlTransport = std::make_unique<StubRpcTransport>();
1444 std::move(transport),
1445 std::move(controlTransport));
1447 const auto auma = LoadGsdml(kAumaGsdml);
1450 setup.
slots = auma.BuildIoSlots();
1456 const auto result = connection.Connect(kPhoenixMac,
true, setup);
1457 ASSERT_TRUE(result.has_value());
1458 ASSERT_EQ(capture->requests.size(), 1U);
1462 const auto generatedBlocks = ExtractBlocks(nrd.payload);
1465 ASSERT_GE(generatedBlocks.size(), 3U);
1466 ASSERT_GE(capturedBlocks.size(), 3U);
1471 EXPECT_EQ(generatedBlocks[1], *FindBlock(capturedBlocks,
static_cast<std::uint16_t
>(
BlockType::IoCrRequest)));
1474 EXPECT_EQ(parsed.iocrType, 1U);
1475 EXPECT_EQ(parsed.iocrReference, 1U);
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);
1484 EXPECT_EQ(generatedBlocks[2], *FindBlock(capturedBlocks,
static_cast<std::uint16_t
>(
BlockType::IoCrRequest), 1U));
1486 EXPECT_EQ(parsed.iocrType, 2U);
1487 EXPECT_EQ(parsed.iocrReference, 2U);
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);
1498TEST(CyclicCaptureTests, DISABLED_BuildOutputIocrBlockMatchesPhoenixAumaCapture)
1500 auto transport = std::make_unique<StubRpcTransport>();
1501 auto* capture = transport.get();
1502 auto controlTransport = std::make_unique<StubRpcTransport>();
1507 std::move(transport),
1508 std::move(controlTransport));
1510 const auto auma = LoadGsdml(kAumaGsdml);
1513 setup.
slots = auma.BuildIoSlots();
1519 const auto result = connection.Connect(kPhoenixMac,
true, setup);
1520 ASSERT_TRUE(result.has_value());
1521 ASSERT_EQ(capture->requests.size(), 1U);
1525 const auto generatedBlocks = ExtractBlocks(nrd.payload);
1528 ASSERT_GE(generatedBlocks.size(), 3U);
1530 const auto* capturedOutput =
1532 ASSERT_NE(capturedOutput,
nullptr);
1533 EXPECT_EQ(generatedBlocks[2], *capturedOutput);
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);
1548TEST(CyclicCaptureTests, BuildExpectedSubmoduleBlocksMatchPhoenixAumaCapture)
1550 auto transport = std::make_unique<StubRpcTransport>();
1551 auto* capture = transport.get();
1552 auto controlTransport = std::make_unique<StubRpcTransport>();
1557 std::move(transport),
1558 std::move(controlTransport));
1560 const auto auma = LoadGsdml(kAumaGsdml);
1563 setup.
slots = auma.BuildIoSlots();
1569 const auto result = connection.Connect(kPhoenixMac,
true, setup);
1570 ASSERT_TRUE(result.has_value());
1571 ASSERT_EQ(capture->requests.size(), 1U);
1575 const auto generatedBlocks = ExtractBlocks(nrd.payload);
1578 std::vector<Bytes> generatedExpected;
1579 std::vector<Bytes> capturedExpected;
1581 for (
const auto& block : generatedBlocks)
1586 generatedExpected.push_back(block);
1590 for (
const auto& block : capturedBlocks)
1595 capturedExpected.push_back(block);
1599 ASSERT_EQ(capturedExpected.size(), 3U);
1600 EXPECT_EQ(generatedExpected, capturedExpected);