PROFINET IO Controller Stack 1.0.0
Modern C++ implementation of a PROFINET IO Controller stack
Loading...
Searching...
No Matches
test_rpc_Integration.cpp
Go to the documentation of this file.
1
26
27#include <array>
28#include <asio.hpp>
29#include <atomic>
30#include <chrono>
31#include <cstdint>
32#include <cstring>
33#include <functional>
34#include <mutex>
35#include <optional>
36#include <ranges>
37#include <string>
38#include <thread>
39#include <utility>
40#include <vector>
41
42#include "profinet/exceptions.h"
43#include "profinet/protocol.h"
44#include "profinet/rpc.h"
45
46using namespace profinet;
47using namespace profinet::rpc;
48
49/*
50class RpcTestServer
51{
52 public:
53 explicit RpcTestServer(asio::io_context& ioContext);
54
55 void Start();
56 void Stop();
57
58 std::uint16_t Port() const;
59
60 Bytes Receive();
61
62 void Send(const Bytes& response);
63
64 private:
65 asio::ip::udp::socket socket;
66 asio::ip::udp::endpoint remoteEndpoint;
67};
68
69class ProfinetRpcTestDevice
70{
71 public:
72 void Start();
73
74 void ExpectConnect();
75 void ExpectRead(...);
76 void ExpectWrite(...);
77
78 void Stop();
79
80 private:
81 ...
82};
83namespace rpc
84{
85
86TEST_F(RpcConIntegrationTest, ConnectAndReadIm0)
87{
88 ProfinetRpcTestDevice device;
89 device.Start();
90
91 RPCCon connection(MakeDeviceDescription(device.Port()));
92
93 connection.Connect(TestMacAddress());
94
95 auto im0 = connection.ReadIm0();
96
97 EXPECT_EQ(im0.OrderId, "...");
98}
99} // namespace rpc
100*/
101
102using namespace std::chrono_literals;
103
104// ---------------------------------------------------------------------------
105// Test constants
106// ---------------------------------------------------------------------------
107
108constexpr std::uint16_t kTestRpcPort = RPC_PORT;
109
110constexpr std::uint32_t kTestApi = 0;
111
112constexpr std::uint16_t kTestSlot = 0;
113constexpr std::uint16_t kTestSubslot = 1;
114
115constexpr std::uint16_t kTestReadIndex = 0xAFF0;
116constexpr std::uint16_t kTestWriteIndex = 0xAFF1;
117
118constexpr std::uint16_t kTestAlarmReference = 0x1234;
119
120// ---------------------------------------------------------------------------
121// Test MAC address
122// ---------------------------------------------------------------------------
123
125{
126 MacAddress mac{};
127
128 mac[0] = 0x02;
129 mac[1] = 0x00;
130 mac[2] = 0x00;
131 mac[3] = 0x00;
132 mac[4] = 0x00;
133 mac[5] = 0x01;
134
135 return mac;
136}
137
138// ---------------------------------------------------------------------------
139// Test device description
140// ---------------------------------------------------------------------------
141
143{
144 dcp::DCPDeviceDescription info(String2Mac("aa:bb:cc:dd:ee:ff"),
145 {});
146
147 info.name = "test-device";
148 info.ip = "127.0.0.1";
149
150 // Arbitrary values used to construct the remote object UUID.
151 info.deviceHigh = 0x1234;
152 info.deviceLow = 0x5678;
153 info.vendorHigh = 0x9ABC;
154 info.vendorLow = 0xDEF0;
155
156 return info;
157}
158
159// ---------------------------------------------------------------------------
160// Helpers
161// ---------------------------------------------------------------------------
162
164{
165 PNNRDData nrd;
166 nrd.argsMaximumStatus = 0;
167 nrd.argsLength = static_cast<std::uint32_t>(payload.size());
168 nrd.maximumCount = 1500;
169 nrd.offset = 0;
170 nrd.actualCount = static_cast<std::uint32_t>(payload.size());
171 nrd.payload = payload;
172
173 return nrd.ToBytes();
174}
175
177 const PNRPCHeader& request,
178 const Bytes& payload)
179{
180 PNRPCHeader response = request;
181
183 response.flags1 = 0x00;
184 response.flags2 = 0x00;
185 response.lengthOfBody = static_cast<std::uint16_t>(payload.size());
186 response.payload = payload;
187
188 return response;
189}
190
192 const PNRPCHeader& request)
193{
194 PNRPCHeader response = request;
195
197 response.flags1 = 0x00;
198 response.flags2 = 0x00;
199 response.lengthOfBody = 0;
200 response.payload.clear();
201
202 return response;
203}
204
206 const PNRPCHeader& request)
207{
208 PNRPCHeader response = request;
209
211 response.flags1 = 0x00;
212 response.flags2 = 0x00;
213 response.lengthOfBody = 0;
214 response.payload.clear();
215
216 return response;
217}
218
220 std::uint32_t argsMaximumStatus,
221 const Bytes& payload = {})
222{
223 PNNRDData nrd;
224 nrd.argsMaximumStatus = argsMaximumStatus;
225 nrd.argsLength = static_cast<std::uint32_t>(payload.size());
226 nrd.maximumCount = 1500;
227 nrd.offset = 0;
228 nrd.actualCount = static_cast<std::uint32_t>(payload.size());
229 nrd.payload = payload;
230
231 return nrd.ToBytes();
232}
233
235{
236 const PNNRDData nrd = PNNRDData::Parse(request.payload);
237 return nrd.payload;
238}
239
240/*
241std::vector<PNBlockHeader> FindBlocks(const Bytes& data)
242{
243 std::vector<PNBlockHeader> result;
244
245 std::size_t offset = 0;
246
247 while (offset + blockHeaderLength <= data.size())
248 {
249 const PNBlockHeader header = PeekBlockHeader(data, offset);
250
251 result.push_back(header);
252
253 const std::size_t next =
254 offset +
255 4 +
256 static_cast<std::size_t>(header.blockLength);
257
258 if (next <= offset || next > data.size())
259 {
260 break;
261 }
262
263 offset = next;
264 }
265
266 return result;
267}
268*/
270{
271 std::size_t offset;
272 std::uint16_t blockType;
273 std::uint16_t blockLength;
274};
275
276std::vector<BlockView> FindBlocks(const Bytes& data)
277{
278 std::vector<BlockView> result;
279
280 std::size_t offset = 0;
281
282 while (offset + 4 <= data.size())
283 {
284 const BlockHeaderView header =
285 PeekBlockHeader(data, offset);
286
287 const std::size_t blockSize =
288 4U + static_cast<std::size_t>(header.blockLength);
289
290 if (blockSize > data.size() - offset)
291 {
292 break;
293 }
294
295 result.push_back({
296 .offset = offset,
297 .blockType = header.blockType,
298 .blockLength = header.blockLength,
299 });
300
301 offset += blockSize;
302 }
303
304 return result;
305}
306
307Bytes GetBlock(const Bytes& data, const BlockView& block)
308{
309 const std::size_t size =
310 4U + static_cast<std::size_t>(block.blockLength);
311
312 return Bytes(
313 data.begin() + block.offset,
314 data.begin() + block.offset + size);
315}
316
318 const Bytes& data,
319 std::uint16_t blockType)
320{
321 const auto blocks = FindBlocks(data);
322
323 for (const auto& block : blocks)
324 {
325 if (block.blockType == blockType)
326 {
327 return true;
328 }
329 }
330
331 return false;
332}
333
334std::size_t CountBlockType(
335 const Bytes& data,
336 std::uint16_t blockType)
337{
338 std::size_t count = 0;
339
340 const auto blocks = FindBlocks(data);
341
342 for (const auto& block : blocks)
343 {
344 if (block.blockType == blockType)
345 {
346 ++count;
347 }
348 }
349
350 return count;
351}
352
353// ---------------------------------------------------------------------------
354// Simulated PROFINET RPC device
355// ---------------------------------------------------------------------------
356
358{
359 public:
361 {
362 std::uint16_t blockType = 0;
363 std::uint16_t controlCommand = 0;
364 std::uint16_t controlBlockProperties = 0;
365 std::array<std::uint8_t, uuidLenght> arUuid{};
366 std::uint16_t sessionKey = 0;
367 };
368
370 {
371 std::uint16_t blockType = 0;
372 std::uint16_t controlCommand = 0;
373 std::uint16_t controlBlockProperties = 0;
374 std::array<std::uint8_t, uuidLenght> arUuid{};
375 std::uint16_t sessionKey = 0;
376 std::uint16_t operationNumber = 0;
377 std::uint16_t packetType = 0;
378 };
379
380 enum class ResponseMode
381 {
382 ConnectOk,
384 RpcFault,
385 RpcReject,
388 Read,
389 Write,
390 AlarmCr,
391 Iocr
392 };
393
395 : mode(mode),
397 {
398 }
399
401 {
402 Stop();
403 }
404
405 void Start()
406 {
407 asio::error_code ec;
408
409 socket.open(
410 asio::ip::udp::v4(),
411 ec);
412
413 ASSERT_FALSE(ec)
414 << "Failed to open test UDP socket: "
415 << ec.message();
416
417 // Use an ephemeral source port. RPCCon's CControl socket owns
418 // RPC_PORT, so the simulated device must not bind to that port.
419 socket.bind(
420 asio::ip::udp::endpoint(
421 asio::ip::udp::v4(),
422 0),
423 ec);
424
425 ASSERT_FALSE(ec)
426 << "Failed to bind test UDP socket: "
427 << ec.message();
428
429 running.store(true);
430
431 worker = std::thread(
432 [this]()
433 {
434 Run();
435 });
436 }
437
438 void Stop()
439 {
440 if (!running.exchange(false))
441 {
442 return;
443 }
444
445 asio::error_code ec;
446
447 if (socket.is_open())
448 {
449 socket.cancel(ec);
450 socket.close(ec);
451 }
452
453 stateCondition.notify_all();
454
455 if (worker.joinable())
456 {
457 worker.join();
458 }
459 }
460
461 // -------------------------------------------------------------------------
462 // General test state
463 // -------------------------------------------------------------------------
464
465 [[nodiscard]] std::size_t RequestCount() const
466 {
467 return requestCount.load();
468 }
469
470 [[nodiscard]] bool SawExpectedConnect() const
471 {
472 return sawExpectedConnect.load();
473 }
474
475 [[nodiscard]] bool SawAlarmCr() const
476 {
477 return sawAlarmCr.load();
478 }
479
480 [[nodiscard]] bool SawInputIocr() const
481 {
482 return sawInputIocr.load();
483 }
484
485 [[nodiscard]] bool SawOutputIocr() const
486 {
487 return sawOutputIocr.load();
488 }
489
490 [[nodiscard]] bool SawExpectedSubmodule() const
491 {
492 return sawExpectedSubmodule.load();
493 }
494
495 [[nodiscard]] bool SawRead() const
496 {
497 return sawRead.load();
498 }
499
500 [[nodiscard]] bool SawWrite() const
501 {
502 return sawWrite.load();
503 }
504
505 [[nodiscard]] std::uint16_t ReadIndex() const
506 {
507 return readIndex.load();
508 }
509
510 [[nodiscard]] std::uint16_t WriteIndex() const
511 {
512 return writeIndex.load();
513 }
514
515 [[nodiscard]] Bytes LastWritePayload() const
516 {
517 std::scoped_lock lock(mutex);
518 return lastWritePayload;
519 }
520
521 // -------------------------------------------------------------------------
522 // CONNECT synchronization
523 // -------------------------------------------------------------------------
524
528 [[nodiscard]] bool WaitForConnect(
529 std::chrono::milliseconds timeout =
530 std::chrono::milliseconds{1000})
531 {
532 std::unique_lock lock(stateMutex);
533
534 return stateCondition.wait_for(
535 lock,
536 timeout,
537 [this]()
538 {
539 return connected.load();
540 });
541 }
542
543 // -------------------------------------------------------------------------
544 // ApplicationReady
545 // -------------------------------------------------------------------------
546
555 const std::array<std::uint8_t, uuidLenght>& arUuid,
556 std::uint16_t sessionKey)
557 {
558 if (!connected.load())
559 {
560 throw std::runtime_error(
561 "Cannot send ApplicationReady before CONNECT");
562 }
563
564 {
565 std::scoped_lock lock(stateMutex);
566
568 {
569 throw std::runtime_error(
570 "No CONNECT request has been received");
571 }
572 }
573
577 arUuid,
578 sessionKey);
579
580 {
581 std::scoped_lock lock(stateMutex);
582
584 {
588 }
589 }
590
591 stateCondition.notify_all();
592 }
593
598 std::chrono::milliseconds timeout =
599 std::chrono::milliseconds{1000}) const
600 {
601 std::unique_lock lock(stateMutex);
602
603 return stateCondition.wait_for(
604 lock,
605 timeout,
606 [this]()
607 {
608 return applicationReadyResponse.has_value();
609 });
610 }
611
613 [[nodiscard]] bool ReceivedApplicationReadyResponse() const
614 {
615 std::scoped_lock lock(stateMutex);
616
617 return applicationReadyResponse.has_value();
618 }
619
621 [[nodiscard]] std::optional<ControlResponse>
623 {
624 std::scoped_lock lock(stateMutex);
625
627 }
628
630 [[nodiscard]] std::optional<ControlRequest>
632 {
633 std::scoped_lock lock(stateMutex);
634
636 }
637
639 [[nodiscard]] std::optional<PNRPCHeader>
641 {
642 std::scoped_lock lock(stateMutex);
643
645 }
646
648 [[nodiscard]] std::optional<PNRPCHeader>
650 {
651 std::scoped_lock lock(stateMutex);
652
654 }
655
657 [[nodiscard]] std::optional<PNNRDData>
659 {
660 std::scoped_lock lock(stateMutex);
661
663 {
664 return std::nullopt;
665 }
666
667 try
668 {
669 return PNNRDData::Parse(
671 }
672 catch (...)
673 {
674 return std::nullopt;
675 }
676 }
677
689 std::uint16_t operation,
690 const Bytes& payload = {},
691 std::optional<PNRPCHeader> packetTemplate = std::nullopt)
692 {
693 PNRPCHeader request;
694
695 {
696 std::scoped_lock lock(stateMutex);
697
698 if (packetTemplate)
699 {
700 request = *packetTemplate;
701 }
702 else if (lastConnectRequest)
703 {
704 request = *lastConnectRequest;
705 }
706 else
707 {
708 throw std::runtime_error(
709 "Cannot send RPC REQUEST: no packet template is available");
710 }
711 }
712
714 request.operationNumber = operation;
715 request.lengthOfBody =
716 static_cast<std::uint16_t>(payload.size());
717 request.payload = payload;
718
719 SendPacket(request);
720
721 return request;
722 }
723
733 std::uint16_t operation,
734 const Bytes& payload = {},
735 std::optional<PNRPCHeader> packetTemplate = std::nullopt)
736 {
737 PNRPCHeader response;
738
739 {
740 std::scoped_lock lock(stateMutex);
741
742 if (packetTemplate)
743 {
744 response = *packetTemplate;
745 }
746 else if (lastConnectRequest)
747 {
748 response = *lastConnectRequest;
749 }
750 else
751 {
752 throw std::runtime_error(
753 "Cannot send RPC RESPONSE: no packet template is available");
754 }
755 }
756
758 response.operationNumber = operation;
759 response.lengthOfBody =
760 static_cast<std::uint16_t>(payload.size());
761 response.payload = payload;
762
763 SendPacket(response);
764 }
765
777 std::uint16_t blockType,
778 std::uint16_t controlCommand,
779 const std::array<std::uint8_t, uuidLenght>& arUuid,
780 std::uint16_t sessionKey,
781 std::uint16_t controlBlockProperties = 0)
782 {
783 const Bytes controlBlock =
785 blockType,
786 controlCommand,
787 arUuid,
788 sessionKey,
789 controlBlockProperties);
790
791 const Bytes nrd =
792 MakeNrdResponse(controlBlock);
793
794 const PNRPCHeader request =
797 nrd);
798
799 {
800 std::scoped_lock lock(stateMutex);
801
803 request;
804
805 if (blockType ==
807 {
809 ParseControlRequest(request);
810 }
811 }
812
813 stateCondition.notify_all();
814 }
815
816 private:
817 // -------------------------------------------------------------------------
818 // Worker
819 // -------------------------------------------------------------------------
820
821 void Run()
822 {
823 while (running)
824 {
826
827 asio::ip::udp::endpoint sender;
828 asio::error_code ec;
829
830 const std::size_t received =
831 socket.receive_from(
832 asio::buffer(buffer),
833 sender,
834 0,
835 ec);
836
837 if (!running)
838 {
839 break;
840 }
841
842 if (ec)
843 {
844 continue;
845 }
846
847 buffer.resize(received);
848
850 buffer,
851 sender);
852 }
853 }
854
855 // -------------------------------------------------------------------------
856 // Receive / dispatch
857 // -------------------------------------------------------------------------
858
860 const Bytes& bytes,
861 const asio::ip::udp::endpoint& sender)
862 {
863 ++requestCount;
864
865 PNRPCHeader packet;
866
867 try
868 {
869 packet =
870 PNRPCHeader::Parse(bytes);
871 }
872 catch (...)
873 {
874 return;
875 }
876
877 // ApplicationReady is initiated by the device, so its response comes
878 // back as an RPC RESPONSE on the same UDP socket.
879 if (packet.packetType == PNRPCHeader::RESPONSE &&
881 {
883 return;
884 }
885
886 // All remaining device handling expects incoming RPC REQUEST packets.
887 if (packet.packetType != PNRPCHeader::REQUEST)
888 {
889 return;
890 }
891
892 switch (mode)
893 {
896 packet,
897 sender,
898 false,
899 false);
900 break;
901
904 packet,
905 sender);
906 break;
907
910 packet,
911 sender);
912 break;
913
916 packet,
917 sender);
918 break;
919
921 // Deliberately do nothing.
922 break;
923
926 packet,
927 sender,
928 true,
929 false);
930 break;
931
934 packet,
935 sender);
936 break;
937
940 packet,
941 sender);
942 break;
943
946 packet,
947 sender,
948 false,
949 true);
950 break;
951
954 packet,
955 sender,
956 false,
957 false);
958 break;
959 }
960 }
961
962 // -------------------------------------------------------------------------
963 // UDP
964 // -------------------------------------------------------------------------
965
967 const PNRPCHeader& response,
968 const asio::ip::udp::endpoint& remote)
969 {
970 const Bytes bytes =
971 response.ToBytes();
972
973 asio::error_code ec;
974
975 socket.send_to(
976 asio::buffer(bytes),
977 remote,
978 0,
979 ec);
980 }
981
982 [[nodiscard]] asio::ip::udp::endpoint
984 {
985 std::scoped_lock lock(stateMutex);
986
987 if (controllerEndpoint.address().is_unspecified())
988 {
989 return {
990 asio::ip::make_address_v4("127.0.0.1"),
991 RPC_PORT};
992 }
993
994 return {
995 controllerEndpoint.address(),
996 RPC_PORT};
997 }
998
999 // -------------------------------------------------------------------------
1000 // RPC error responses
1001 // -------------------------------------------------------------------------
1002
1004 const PNRPCHeader& request,
1005 const asio::ip::udp::endpoint& sender)
1006 {
1008 MakeRpcFault(request),
1009 sender);
1010 }
1011
1013 const PNRPCHeader& request,
1014 const asio::ip::udp::endpoint& sender)
1015 {
1017 MakeRpcReject(request),
1018 sender);
1019 }
1020
1022 const PNRPCHeader& request,
1023 const asio::ip::udp::endpoint& sender)
1024 {
1025 const Bytes nrd =
1027 0x81DB0307);
1028
1029 const PNRPCHeader response =
1031 request,
1032 nrd);
1033
1035 response,
1036 sender);
1037 }
1038
1039 // -------------------------------------------------------------------------
1040 // CONNECT
1041 // -------------------------------------------------------------------------
1042
1044 const PNRPCHeader& request,
1045 const asio::ip::udp::endpoint& sender,
1046 bool sendRequestBeforeResponse,
1047 bool checkAlarmCr)
1048 {
1050 {
1051 return;
1052 }
1053
1054 const Bytes nrdPayload =
1055 ExtractNrdPayload(request);
1056
1057 const bool hasArBlock =
1059 nrdPayload,
1060 BlockType::ApplicationRelationData);
1061
1062 sawExpectedConnect.store(
1063 hasArBlock);
1064
1065 if (!hasArBlock)
1066 {
1067 return;
1068 }
1069
1070 if (checkAlarmCr)
1071 {
1072 const bool hasAlarmCr =
1074 nrdPayload,
1075 BlockType::AlarmCrRequest);
1076
1077 sawAlarmCr.store(
1078 hasAlarmCr);
1079 }
1080
1081 if (mode == ResponseMode::Iocr)
1082 {
1083 bool sawInput = false;
1084 bool sawOutput = false;
1085
1086 std::size_t offset = 0;
1087
1088 while (offset + blockHeaderLength <= nrdPayload.size())
1089 {
1090 const BlockHeaderView header =
1092 nrdPayload,
1093 offset);
1094
1095 const std::size_t blockSize =
1096 4U +
1097 static_cast<std::size_t>(
1098 header.blockLength);
1099
1100 if (blockSize >
1101 nrdPayload.size() - offset)
1102 {
1103 break;
1104 }
1105
1106 if (header.blockType ==
1108 {
1109 const Bytes blockBytes(
1110 nrdPayload.begin() +
1111 offset),
1112 nrdPayload.begin() +
1113
1114 offset + blockSize));
1115
1116 try
1117 {
1118 const PNIOCRBlockReqHeader iocr =
1120 blockBytes);
1121
1122 if (iocr.iocrType == 1)
1123 {
1124 sawInput = true;
1125 }
1126 else if (iocr.iocrType == 2)
1127 {
1128 sawOutput = true;
1129 }
1130 }
1131 catch (const std::exception&)
1132 {
1133 // Ignore malformed IOCR blocks.
1134 }
1135 }
1136
1137 offset += blockSize;
1138 }
1139
1140 sawInputIocr.store(sawInput);
1141 sawOutputIocr.store(sawOutput);
1142
1145 nrdPayload,
1146 BLOCK_EXPECTED_SUBMODULE_REQ));
1147 }
1148
1149 {
1150 std::scoped_lock lock(stateMutex);
1151
1153 request;
1154
1156 sender;
1157
1158 connected.store(true);
1159 }
1160
1161 stateCondition.notify_all();
1162
1163 if (sendRequestBeforeResponse)
1164 {
1165 // Send the original request back as an unsolicited REQUEST.
1166 // RPCCon::SendReceive() should ignore it and continue waiting
1167 // for the actual RESPONSE.
1168 const Bytes unsolicited =
1169 request.ToBytes();
1170
1171 asio::error_code ec;
1172
1173 socket.send_to(
1174 asio::buffer(unsolicited),
1175 sender,
1176 0,
1177 ec);
1178
1179 if (ec)
1180 {
1181 return;
1182 }
1183 }
1184
1185 const Bytes nrd =
1186 MakeNrdResponse({});
1187
1188 const PNRPCHeader response =
1190 request,
1191 nrd);
1192
1194 response,
1195 sender);
1196 }
1197
1198 // -------------------------------------------------------------------------
1199 // READ
1200 // -------------------------------------------------------------------------
1201
1203 const PNRPCHeader& request,
1204 const asio::ip::udp::endpoint& sender)
1205 {
1206 if (request.operationNumber != PNRPCHeader::READ)
1207 {
1208 return;
1209 }
1210
1211 const Bytes nrdPayload =
1212 ExtractNrdPayload(request);
1213
1214 PNIODHeader readRequest;
1215
1216 try
1217 {
1218 readRequest =
1220 nrdPayload);
1221 }
1222 catch (...)
1223 {
1224 return;
1225 }
1226
1227 sawRead.store(true);
1228 readIndex.store(readRequest.index);
1229
1230 Bytes recordData = {
1231 0x01,
1232 0x02,
1233 0x03,
1234 0x04,
1235 0x05,
1236 0x06,
1237 0x07,
1238 0x08};
1239
1240 readRequest.length =
1241 static_cast<std::uint32_t>(
1242 recordData.size());
1243
1244 readRequest.payload =
1245 recordData;
1246
1247 const Bytes responsePayload =
1248 readRequest.ToBytes();
1249
1250 const Bytes responseNrd =
1252 0,
1253 responsePayload);
1254
1255 const PNRPCHeader response =
1257 request,
1258 responseNrd);
1259
1261 response,
1262 sender);
1263 }
1264
1265 // -------------------------------------------------------------------------
1266 // WRITE
1267 // -------------------------------------------------------------------------
1268
1270 const PNRPCHeader& request,
1271 const asio::ip::udp::endpoint& sender)
1272 {
1273 if (request.operationNumber != PNRPCHeader::WRITE)
1274 {
1275 return;
1276 }
1277
1278 const Bytes nrdPayload =
1279 ExtractNrdPayload(request);
1280
1281 PNIODHeader writeRequest;
1282
1283 try
1284 {
1285 writeRequest =
1287 nrdPayload);
1288 }
1289 catch (...)
1290 {
1291 return;
1292 }
1293
1294 sawWrite.store(true);
1295 writeIndex.store(writeRequest.index);
1296
1297 {
1298 std::scoped_lock lock(mutex);
1299
1301 writeRequest.payload;
1302 }
1303
1304 const Bytes responseNrd =
1306
1307 const PNRPCHeader response =
1309 request,
1310 responseNrd);
1311
1313 response,
1314 sender);
1315 }
1316
1317 // -------------------------------------------------------------------------
1318 // ApplicationReady
1319 // -------------------------------------------------------------------------
1320
1322 const std::array<std::uint8_t, uuidLenght>& arUuid,
1323 std::uint16_t sessionKey)
1324 {
1325 PNBlockHeader block;
1326 block.blockType =
1328 block.blockLength = 28;
1329 block.blockVersionHigh = 0x01;
1330 block.blockVersionLow = 0x00;
1331
1332 PNIODReleaseBlock controlBlock;
1333 std::ranges::copy(block.ToBytes() | std::views::take(blockHeaderLength), controlBlock.blockHeader.begin());
1334
1335 controlBlock.padding1 = 0;
1336 controlBlock.arUuid = arUuid;
1337 controlBlock.sessionKey = sessionKey;
1338 controlBlock.padding2 = 0;
1339 controlBlock.controlCommand =
1341 controlBlock.controlBlockProperties = 0;
1342
1343 return controlBlock.ToBytes();
1344 }
1345
1346 static std::optional<ControlRequest>
1348 const PNRPCHeader& request)
1349 {
1350 if (request.packetType !=
1352 {
1353 return std::nullopt;
1354 }
1355
1356 if (request.operationNumber !=
1358 {
1359 return std::nullopt;
1360 }
1361
1362 PNNRDData nrd;
1363
1364 try
1365 {
1366 nrd =
1368 request.payload);
1369 }
1370 catch (...)
1371 {
1372 return std::nullopt;
1373 }
1374
1375 constexpr std::size_t kControlBlockSize = 32;
1376
1377 if (nrd.payload.size() <
1378 kControlBlockSize)
1379 {
1380 return std::nullopt;
1381 }
1382
1383 const Bytes& data =
1384 nrd.payload;
1385
1386 ControlRequest result;
1387
1388 result.blockType =
1389 static_cast<std::uint16_t>(
1390 (static_cast<std::uint16_t>(data[0]) << OneOctetShift) |
1391 static_cast<std::uint16_t>(data[1]));
1392
1393 if (result.blockType !=
1395 {
1396 return std::nullopt;
1397 }
1398
1399 result.arUuid = {};
1400
1401 std::ranges::copy(data | std::views::drop(8) | std::views::take(uuidLenght), result.arUuid.begin());
1402
1403 result.sessionKey =
1404 static_cast<std::uint16_t>(
1405 (static_cast<std::uint16_t>(data[24]) << OneOctetShift) |
1406 static_cast<std::uint16_t>(data[25]));
1407
1408 result.controlCommand =
1409 static_cast<std::uint16_t>(
1410 (static_cast<std::uint16_t>(data[28]) << OneOctetShift) |
1411 static_cast<std::uint16_t>(data[29]));
1412
1413 result.controlBlockProperties =
1414 static_cast<std::uint16_t>(
1415 (static_cast<std::uint16_t>(data[30]) << OneOctetShift) |
1416 static_cast<std::uint16_t>(data[31]));
1417
1418 return result;
1419 }
1420
1421 static std::optional<ControlResponse>
1423 const PNRPCHeader& response)
1424 {
1425 if (response.packetType !=
1427 {
1428 return std::nullopt;
1429 }
1430
1431 if (response.operationNumber !=
1433 {
1434 return std::nullopt;
1435 }
1436
1437 PNNRDData nrd;
1438
1439 try
1440 {
1441 nrd =
1443 response.payload);
1444 }
1445 catch (...)
1446 {
1447 return std::nullopt;
1448 }
1449
1450 constexpr std::size_t kControlBlockSize = 32;
1451
1452 if (nrd.payload.size() <
1453 kControlBlockSize)
1454 {
1455 return std::nullopt;
1456 }
1457
1458 const Bytes& data =
1459 nrd.payload;
1460
1461 ControlResponse result;
1462
1463 result.blockType =
1464 static_cast<std::uint16_t>(
1465 (static_cast<std::uint16_t>(data[0]) << OneOctetShift) |
1466 static_cast<std::uint16_t>(data[1]));
1467
1468 if (result.blockType !=
1470 {
1471 return std::nullopt;
1472 }
1473
1474 std::ranges::copy(data | std::views::drop(8) | std::views::take(uuidLenght), result.arUuid.begin());
1475
1476 result.sessionKey =
1477 static_cast<std::uint16_t>(
1478 (static_cast<std::uint16_t>(data[24]) << OneOctetShift) |
1479 static_cast<std::uint16_t>(data[25]));
1480
1481 result.controlCommand =
1482 static_cast<std::uint16_t>(
1483 (static_cast<std::uint16_t>(data[28]) << OneOctetShift) |
1484 static_cast<std::uint16_t>(data[29]));
1485
1486 result.controlBlockProperties =
1487 static_cast<std::uint16_t>(
1488 (static_cast<std::uint16_t>(data[30]) << OneOctetShift) |
1489 static_cast<std::uint16_t>(data[31]));
1490
1491 return result;
1492 }
1493
1495 const PNRPCHeader& response)
1496 {
1497 const auto parsed =
1499 response);
1500
1501 if (!parsed)
1502 {
1503 return;
1504 }
1505
1506 {
1507 std::scoped_lock lock(stateMutex);
1508
1510 response;
1511
1513 parsed;
1514 }
1515
1516 stateCondition.notify_all();
1517 }
1519 const PNRPCHeader& packet)
1520 {
1521 const Bytes bytes =
1522 packet.ToBytes();
1523
1524 const asio::ip::udp::endpoint target =
1526
1527 asio::error_code ec;
1528
1529 socket.send_to(
1530 asio::buffer(bytes),
1531 target,
1532 0,
1533 ec);
1534
1535 if (ec)
1536 {
1537 throw std::system_error(
1538 ec,
1539 "Failed to send RPC packet");
1540 }
1541 }
1542
1544 std::uint16_t blockType,
1545 std::uint16_t controlCommand,
1546 const std::array<std::uint8_t, uuidLenght>& arUuid,
1547 std::uint16_t sessionKey,
1548 std::uint16_t controlBlockProperties)
1549 {
1550 PNBlockHeader block;
1551 block.blockType = blockType;
1552 block.blockLength = 28;
1553 block.blockVersionHigh = 0x01;
1554 block.blockVersionLow = 0x00;
1555
1556 PNIODReleaseBlock controlBlock;
1557
1558 std::ranges::copy(block.ToBytes() | std::views::take(blockHeaderLength), controlBlock.blockHeader.begin());
1559
1560 controlBlock.padding1 = 0;
1561 controlBlock.arUuid = arUuid;
1562 controlBlock.sessionKey = sessionKey;
1563 controlBlock.padding2 = 0;
1564 controlBlock.controlCommand = controlCommand;
1565 controlBlock.controlBlockProperties =
1566 controlBlockProperties;
1567
1568 return controlBlock.ToBytes();
1569 }
1570
1571 private:
1573
1574 asio::io_context ioContext;
1575 asio::ip::udp::socket socket;
1576 std::thread worker;
1577
1578 std::atomic_bool running{false};
1579 std::atomic<std::size_t> requestCount{0};
1580
1581 std::atomic_bool connected{false};
1582
1583 std::atomic_bool sawExpectedConnect{false};
1584 std::atomic_bool sawAlarmCr{false};
1585 std::atomic_bool sawInputIocr{false};
1586 std::atomic_bool sawOutputIocr{false};
1587 std::atomic_bool sawExpectedSubmodule{false};
1588 std::atomic_bool sawRead{false};
1589 std::atomic_bool sawWrite{false};
1590
1591 std::atomic<std::uint16_t> readIndex{0};
1592 std::atomic<std::uint16_t> writeIndex{0};
1593
1594 mutable std::mutex mutex;
1596
1597 mutable std::mutex stateMutex;
1598 mutable std::condition_variable stateCondition;
1599
1600 asio::ip::udp::endpoint controllerEndpoint;
1601
1602 std::optional<PNRPCHeader> lastConnectRequest;
1603
1604 std::optional<PNRPCHeader>
1606
1607 std::optional<PNRPCHeader>
1609
1610 std::optional<ControlRequest>
1612
1613 std::optional<ControlResponse>
1615};
1616
1617// ---------------------------------------------------------------------------
1618// IOCR test setup
1619// ---------------------------------------------------------------------------
1620
1622{
1623 IOCRSetup setup{};
1624
1625 // The values here intentionally mirror the three-slot configuration
1626 // used by the AUMA CDT device in the integration work.
1627
1628 IOSlot slot0{};
1629 slot0.slot = 0;
1630 slot0.subslot = 1;
1631 slot0.moduleIdent = 0x00008001;
1632 slot0.submoduleIdent = 0x00000000;
1633 slot0.inputLength = 0;
1634 slot0.outputLength = 0;
1635
1636 IOSlot slot1{};
1637 slot1.slot = 1;
1638 slot1.subslot = 1;
1639 slot1.moduleIdent = 0x00008000;
1640 slot1.submoduleIdent = 0x00000022;
1641 slot1.inputLength = 34;
1642 slot1.outputLength = 0;
1643
1644 IOSlot slot2{};
1645 slot2.slot = 2;
1646 slot2.subslot = 1;
1647 slot2.moduleIdent = 0x00008100;
1648 slot2.submoduleIdent = 0x00000011;
1649 slot2.inputLength = 0;
1650 slot2.outputLength = 16;
1651
1652 setup.slots.push_back(slot0);
1653 setup.slots.push_back(slot1);
1654 setup.slots.push_back(slot2);
1655
1656 return setup;
1657}
1658class RpcConIntegrationTest : public ::testing::Test
1659{
1660 protected:
1661 void SetUp() override
1662 {
1663 options.timeoutSec = 1.0;
1664
1666
1667 testDevice =
1668 std::make_unique<TestRpcDevice>(
1670
1671 connection =
1672 std::make_unique<RPCCon>(
1673 info.value(),
1674 options);
1675 }
1676
1677 void TearDown() override
1678 {
1679 if (connection)
1680 {
1681 connection->Close();
1682 }
1683 if (testDevice)
1684 {
1685 testDevice->Stop();
1686 }
1687 }
1688
1689 void Connect()
1690 {
1691 ASSERT_NE(
1692 connection,
1693 nullptr);
1694 ASSERT_NE(testDevice, nullptr);
1695
1696 ASSERT_NO_THROW(
1697 connection->Connect(
1698 TestMac()));
1699 }
1701 std::optional<dcp::DCPDeviceDescription> info;
1702 std::unique_ptr<RPCCon> connection;
1703 std::unique_ptr<TestRpcDevice> testDevice;
1704};
1705
1706// =============================================================================
1707// CONNECT
1708// =============================================================================
1709
1710TEST_F(RpcConIntegrationTest, ConnectEstablishesApplicationRelationship)
1711{
1712 TestRpcDevice device(
1714
1715 device.Start();
1716
1717 auto info = MakeTestDeviceDescription();
1718
1719 RPCCon connection(
1720 info, options);
1721
1722 ASSERT_FALSE(
1723 connection.IsConnected());
1724
1725 const auto result =
1726 connection.Connect(TestMac());
1727
1728 EXPECT_FALSE(result.has_value());
1729 EXPECT_TRUE(connection.IsConnected());
1730
1731 EXPECT_EQ(
1732 connection.Info().name,
1733 "test-device");
1734
1735 EXPECT_NE(
1736 connection.SessionKey(),
1737 0);
1738
1739 EXPECT_TRUE(
1740 device.SawExpectedConnect());
1741
1742 connection.Disconnect();
1743
1744 EXPECT_FALSE(
1745 connection.IsConnected());
1746}
1747
1748// =============================================================================
1749// CONNECT requires source MAC for initial connection
1750// =============================================================================
1751
1752TEST_F(RpcConIntegrationTest, ConnectRequiresSourceMac)
1753{
1754 TestRpcDevice device(
1756
1757 device.Start();
1758
1759 RPCCon connection(
1761 options);
1762
1763 EXPECT_THROW(connection.Connect(), std::invalid_argument);
1764
1765 EXPECT_FALSE(
1766 connection.IsConnected());
1767}
1768
1769// =============================================================================
1770// CONNECT PNIO error
1771// =============================================================================
1772
1773TEST_F(RpcConIntegrationTest, ConnectConvertsPnioErrorToRpcConnectionError)
1774{
1775 TestRpcDevice device(
1777
1778 device.Start();
1779
1780 RPCCon connection(
1782 options);
1783
1784 EXPECT_THROW(
1785 connection.Connect(TestMac()),
1787
1788 EXPECT_FALSE(
1789 connection.IsConnected());
1790
1791 EXPECT_TRUE(
1792 device.SawExpectedConnect());
1793}
1794
1795// =============================================================================
1796// RPC FAULT
1797// =============================================================================
1798
1799TEST_F(RpcConIntegrationTest, ConnectConvertsRpcFault)
1800{
1801 TestRpcDevice device(
1803
1804 device.Start();
1805
1806 RPCCon connection(
1808 options);
1809
1810 EXPECT_THROW(
1811 connection.Connect(TestMac()),
1813
1814 EXPECT_FALSE(
1815 connection.IsConnected());
1816}
1817
1818// =============================================================================
1819// RPC REJECT
1820// =============================================================================
1821
1822TEST_F(RpcConIntegrationTest, ConnectConvertsRpcReject)
1823{
1824 TestRpcDevice device(
1826
1827 device.Start();
1828
1829 RPCCon connection(
1831 options);
1832
1833 EXPECT_THROW(
1834 connection.Connect(TestMac()),
1836
1837 EXPECT_FALSE(
1838 connection.IsConnected());
1839}
1840
1841// =============================================================================
1842// RPC REQUEST before RESPONSE
1843//
1844// RPCCon::SendReceive() explicitly ignores RPC REQUEST packets and keeps
1845// waiting for the actual RESPONSE. Verify that behavior here.
1846// =============================================================================
1847
1848TEST_F(RpcConIntegrationTest, IgnoresIncomingRpcRequestBeforeResponse)
1849{
1850 TestRpcDevice device(
1852
1853 device.Start();
1854
1855 RPCCon connection(
1857 options);
1858
1859 EXPECT_NO_THROW(
1860 connection.Connect(TestMac()));
1861
1862 EXPECT_TRUE(
1863 connection.IsConnected());
1864
1865 EXPECT_TRUE(
1866 device.SawExpectedConnect());
1867
1868 EXPECT_GE(
1869 device.RequestCount(),
1870 1U);
1871}
1872
1873// =============================================================================
1874// Timeout
1875// =============================================================================
1876
1877TEST_F(RpcConIntegrationTest, ConnectTimesOutWhenDeviceDoesNotRespond)
1878{
1879 TestRpcDevice device(
1881
1882 device.Start();
1883 RPCConOptions testOptions{.timeoutSec = 0.05};
1884
1885 RPCCon connection(
1887 testOptions);
1888
1889 EXPECT_THROW(
1890 connection.Connect(TestMac()),
1892
1893 EXPECT_FALSE(
1894 connection.IsConnected());
1895
1896 EXPECT_GE(
1897 device.RequestCount(),
1898 1U);
1899}
1900
1901// =============================================================================
1902// AlarmCR request
1903// =============================================================================
1904
1905TEST_F(RpcConIntegrationTest, ConnectWithAlarmCrSendsAlarmCrBlock)
1906{
1907 TestRpcDevice device(
1909
1910 device.Start();
1911
1912 RPCCon connection(
1914 options);
1915
1916 // The test device validates that AlarmCRBlockReq was included.
1917 //
1918 // The simulated response deliberately does not include AlarmCRBlockRes,
1919 // so HasAlarmCr() remains false. A separate AlarmCR response test can
1920 // be added once the device-side response structure is exposed in the
1921 // protocol types.
1922 EXPECT_NO_THROW(
1923 connection.Connect(
1924 TestMac(),
1925 true));
1926
1927 EXPECT_TRUE(
1928 connection.IsConnected());
1929
1930 EXPECT_TRUE(
1931 device.SawExpectedConnect());
1932
1933 EXPECT_TRUE(
1934 device.SawAlarmCr());
1935
1936 EXPECT_FALSE(
1937 connection.HasAlarmCr());
1938}
1939
1940// =============================================================================
1941// IOCR / ExpectedSubmodule
1942// =============================================================================
1943
1944TEST_F(RpcConIntegrationTest, ConnectWithIocrSendsIocrAndExpectedSubmoduleBlocks)
1945{
1946 TestRpcDevice device(
1948
1949 device.Start();
1950
1951 RPCCon connection(
1953 options);
1954
1955 const IOCRSetup setup =
1957
1958 const auto result =
1959 connection.Connect(
1960 TestMac(),
1961 false,
1962 setup);
1963
1964 ASSERT_TRUE(result.has_value());
1965
1966 EXPECT_TRUE(
1967 connection.IsConnected());
1968
1969 EXPECT_TRUE(
1970 device.SawExpectedConnect());
1971
1972 EXPECT_TRUE(
1973 device.SawInputIocr());
1974
1975 EXPECT_TRUE(
1976 device.SawOutputIocr());
1977
1978 EXPECT_TRUE(
1979 device.SawExpectedSubmodule());
1980
1981 EXPECT_EQ(
1982 result->arUuid,
1983 connection.ArUuid());
1984
1985 EXPECT_EQ(
1986 result->sessionKey,
1987 connection.SessionKey());
1988}
1989
1990// =============================================================================
1991// READ
1992// =============================================================================
1993
1994TEST_F(RpcConIntegrationTest, ReadSendsIodReadAndReturnsPayload)
1995{
1996 TestRpcDevice device(
1998
1999 device.Start();
2000
2001 RPCCon connection(
2003 options);
2004
2005 ASSERT_NO_THROW(
2006 connection.Connect(TestMac()));
2007
2008 // The device thread above only handles CONNECT. Stop it and replace it
2009 // with a READ-capable device would require reconnecting RPCCon to a new
2010 // port. Because the current RPCCon hardcodes RPC_PORT, perform this test
2011 // against a server that handles both CONNECT and READ below.
2012 device.Stop();
2013
2014 class ConnectReadDevice : public TestRpcDevice
2015 {
2016 public:
2017 ConnectReadDevice()
2018 : TestRpcDevice(ResponseMode::Read)
2019 {
2020 }
2021 };
2022
2023 // The current test server's ResponseMode::Read intentionally handles
2024 // READ packets but not CONNECT. Therefore this test focuses on the
2025 // transport/RPC path after an externally established AR and is disabled
2026 // below until the simulated device is made stateful.
2027 GTEST_SKIP()
2028 << "Enable once the test device supports CONNECT + READ in one "
2029 "stateful session.";
2030}
2031
2032// =============================================================================
2033// WRITE
2034// =============================================================================
2035
2036TEST_F(RpcConIntegrationTest, WriteSendsIodWritePayload)
2037{
2038 GTEST_SKIP()
2039 << "Enable once the test device supports CONNECT + WRITE in one "
2040 "stateful session.";
2041}
2042
2043// =============================================================================
2044// ReadImplicit
2045// =============================================================================
2046
2047TEST_F(RpcConIntegrationTest, ImplicitReadRequiresStatefulSimulatedDevice)
2048{
2049 GTEST_SKIP()
2050 << "Enable once the simulated device supports a complete implicit "
2051 "IODReadReq/IODReadRes exchange.";
2052}
2053
2054// =============================================================================
2055// Disconnect
2056// =============================================================================
2057
2058TEST_F(RpcConIntegrationTest, DisconnectClosesApplicationRelationship)
2059{
2060 TestRpcDevice device(
2062
2063 device.Start();
2064
2065 RPCCon connection(
2067 options);
2068
2069 ASSERT_NO_THROW(
2070 connection.Connect(TestMac()));
2071
2072 ASSERT_TRUE(
2073 connection.IsConnected());
2074
2075 EXPECT_NO_THROW(
2076 connection.Disconnect());
2077
2078 EXPECT_FALSE(
2079 connection.IsConnected());
2080
2081 // Disconnect() is explicitly best-effort and does not require
2082 // a response from the simulated device.
2083}
2084
2085// =============================================================================
2086// Close / RAII
2087// =============================================================================
2088
2090{
2091 TestRpcDevice device(
2093
2094 device.Start();
2095
2096 RPCCon connection(
2098 options);
2099
2100 ASSERT_NO_THROW(
2101 connection.Connect(TestMac()));
2102
2103 EXPECT_TRUE(
2104 connection.IsConnected());
2105
2106 EXPECT_NO_THROW(
2107 connection.Close());
2108
2109 EXPECT_FALSE(
2110 connection.IsConnected());
2111
2112 // Close() should be safe to call repeatedly.
2113 EXPECT_NO_THROW(
2114 connection.Close());
2115
2116 EXPECT_FALSE(
2117 connection.IsConnected());
2118}
2119
2120// =============================================================================
2121// IoContext
2122// =============================================================================
2123
2125{
2126 RPCCon connection(
2128 options);
2129
2130 auto& io = connection.IoContext();
2131
2132 EXPECT_EQ(
2133 &io,
2134 &connection.IoContext());
2135}
2136
2137// =============================================================================
2138// Hardware-oriented configuration sanity test
2139//
2140// This does not contact hardware. It simply verifies that our test IOCR
2141// configuration contains the same slot structure used by the integration
2142// work with the AUMA actuator.
2143// =============================================================================
2144
2145TEST_F(RpcConIntegrationTest, AumaLikeIocrConfigurationHasExpectedSlots)
2146{
2147 const IOCRSetup setup =
2149
2150 ASSERT_EQ(
2151 setup.slots.size(),
2152 3U);
2153
2154 EXPECT_EQ(
2155 setup.slots[0].slot,
2156 0);
2157
2158 EXPECT_EQ(
2159 setup.slots[0].moduleIdent,
2160 0x00008001U);
2161
2162 EXPECT_EQ(
2163 setup.slots[1].slot,
2164 1);
2165
2166 EXPECT_EQ(
2167 setup.slots[1].moduleIdent,
2168 0x00008000U);
2169
2170 EXPECT_EQ(
2171 setup.slots[1].submoduleIdent,
2172 0x00000022U);
2173
2174 EXPECT_EQ(
2175 setup.slots[1].inputLength,
2176 34U);
2177
2178 EXPECT_EQ(
2179 setup.slots[1].outputLength,
2180 0U);
2181
2182 EXPECT_EQ(
2183 setup.slots[2].slot,
2184 2);
2185
2186 EXPECT_EQ(
2187 setup.slots[2].moduleIdent,
2188 0x00008100U);
2189
2190 EXPECT_EQ(
2191 setup.slots[2].submoduleIdent,
2192 0x00000011U);
2193
2194 EXPECT_EQ(
2195 setup.slots[2].inputLength,
2196 0U);
2197
2198 EXPECT_EQ(
2199 setup.slots[2].outputLength,
2200 16U);
2201}
2202
2203// -----------------------------------------------------------------------------
2204// ApplicationReady integration tests
2205// -----------------------------------------------------------------------------
2206
2217{
2218 ASSERT_NO_THROW(
2219 Connect());
2220
2221 ASSERT_TRUE(
2222 connection->IsConnected());
2223
2224 ASSERT_TRUE(
2225 testDevice->WaitForConnect());
2226
2227 ASSERT_NO_THROW(
2228 testDevice->SendApplicationReadyRequest(
2229 connection->ArUuid(),
2230 connection->SessionKey()));
2231
2232 EXPECT_NO_THROW(
2233 connection->ApplicationReady(1.0));
2234
2235 EXPECT_TRUE(
2236 testDevice->ReceivedApplicationReadyResponse());
2237
2238 const auto response =
2239 testDevice->ApplicationReadyResponse();
2240
2241 ASSERT_TRUE(
2242 response.has_value());
2243
2244 EXPECT_EQ(
2245 response->blockType,
2247
2248 EXPECT_EQ(
2249 response->controlCommand,
2251
2252 EXPECT_EQ(
2253 response->arUuid,
2254 connection->ArUuid());
2255
2256 EXPECT_EQ(
2257 response->sessionKey,
2258 connection->SessionKey());
2259}
2260
2269TEST_F(RpcConIntegrationTest, ApplicationReadyReceivesCorrectRequest)
2270{
2271 ASSERT_NO_THROW(
2272 Connect());
2273
2274 ASSERT_TRUE(
2275 testDevice->WaitForConnect());
2276
2277 ASSERT_NO_THROW(
2278 testDevice->SendApplicationReadyRequest(
2279 connection->ArUuid(),
2280 connection->SessionKey()));
2281
2282 EXPECT_NO_THROW(
2283 connection->ApplicationReady(1.0));
2284
2285 const auto request =
2286 testDevice->LastApplicationReadyRequest();
2287
2288 ASSERT_TRUE(
2289 request.has_value());
2290
2291 EXPECT_EQ(
2292 request->blockType,
2294
2295 EXPECT_EQ(
2296 request->controlCommand,
2298
2299 EXPECT_EQ(
2300 request->arUuid,
2301 connection->ArUuid());
2302
2303 EXPECT_EQ(
2304 request->sessionKey,
2305 connection->SessionKey());
2306
2307 EXPECT_EQ(
2308 request->controlBlockProperties,
2309 0);
2310}
2311
2320TEST_F(RpcConIntegrationTest, ApplicationReadyResponseMatchesRequest)
2321{
2322 ASSERT_NO_THROW(
2323 Connect());
2324
2325 ASSERT_TRUE(
2326 testDevice->WaitForConnect());
2327
2328 ASSERT_NO_THROW(
2329 testDevice->SendApplicationReadyRequest(
2330 connection->ArUuid(),
2331 connection->SessionKey()));
2332
2333 EXPECT_NO_THROW(
2334 connection->ApplicationReady(1.0));
2335
2336 const auto request =
2337 testDevice->LastApplicationReadyRpcRequest();
2338
2339 const auto response =
2340 testDevice->LastApplicationReadyRpcResponse();
2341
2342 ASSERT_TRUE(
2343 request.has_value());
2344
2345 ASSERT_TRUE(
2346 response.has_value());
2347
2348 EXPECT_EQ(
2349 response->packetType,
2351
2352 EXPECT_EQ(
2353 response->version,
2354 request->version);
2355 /*
2356 EXPECT_EQ(
2357 response->drep,
2358 request->drep);
2359
2360*/
2361 EXPECT_EQ(
2362 response->objectUuid,
2363 request->objectUuid);
2364
2365 EXPECT_EQ(
2366 response->interfaceUuid,
2367 request->interfaceUuid);
2368
2369 EXPECT_EQ(
2370 response->activityUuid,
2371 request->activityUuid);
2372
2373 EXPECT_EQ(
2374 response->sequenceNumber,
2375 request->sequenceNumber);
2376
2377 EXPECT_EQ(
2378 response->operationNumber,
2380
2381 EXPECT_EQ(
2382 response->interfaceHint,
2383 0xFFFF);
2384
2385 EXPECT_EQ(
2386 response->activityHint,
2387 0xFFFF);
2388}
2389
2395TEST_F(RpcConIntegrationTest, ApplicationReadyResponseHasSuccessfulNrdStatus)
2396{
2397 ASSERT_NO_THROW(
2398 Connect());
2399
2400 ASSERT_TRUE(
2401 testDevice->WaitForConnect());
2402
2403 ASSERT_NO_THROW(
2404 testDevice->SendApplicationReadyRequest(
2405 connection->ArUuid(),
2406 connection->SessionKey()));
2407
2408 EXPECT_NO_THROW(
2409 connection->ApplicationReady(1.0));
2410
2411 const auto nrd =
2412 testDevice->LastApplicationReadyResponseNrd();
2413
2414 ASSERT_TRUE(
2415 nrd.has_value());
2416
2417 EXPECT_EQ(
2418 nrd->argsMaximumStatus,
2419 0U);
2420
2421 EXPECT_EQ(
2422 nrd->offset,
2423 0U);
2424
2425 EXPECT_EQ(
2426 nrd->actualCount,
2427 nrd->argsLength);
2428
2429 EXPECT_EQ(
2430 nrd->maximumCount,
2431 nrd->argsLength);
2432}
2433
2439TEST_F(RpcConIntegrationTest, ApplicationReadyTimesOut)
2440{
2441 ASSERT_NO_THROW(
2442 Connect());
2443
2444 ASSERT_TRUE(
2445 testDevice->WaitForConnect());
2446
2447 EXPECT_THROW(
2448 connection->ApplicationReady(
2449 std::chrono::milliseconds{50}.count() / 1000.0),
2451}
2452
2461TEST_F(RpcConIntegrationTest, ApplicationReadyIgnoresNonControlRequest)
2462{
2463 ASSERT_NO_THROW(
2464 Connect());
2465
2466 ASSERT_TRUE(
2467 testDevice->WaitForConnect());
2468
2469 testDevice->SendRpcRequest(
2471
2472 testDevice->SendApplicationReadyRequest(
2473 connection->ArUuid(),
2474 connection->SessionKey());
2475
2476 EXPECT_NO_THROW(
2477 connection->ApplicationReady(1.0));
2478
2479 EXPECT_TRUE(
2480 testDevice->ReceivedApplicationReadyResponse());
2481
2482 const auto response =
2483 testDevice->ApplicationReadyResponse();
2484
2485 ASSERT_TRUE(
2486 response.has_value());
2487
2488 EXPECT_EQ(
2489 response->operationNumber,
2491
2492 EXPECT_EQ(
2493 response->controlCommand,
2495}
2496
2502TEST_F(RpcConIntegrationTest, ApplicationReadyIgnoresRpcResponse)
2503{
2504 ASSERT_NO_THROW(
2505 Connect());
2506
2507 ASSERT_TRUE(
2508 testDevice->WaitForConnect());
2509
2510 testDevice->SendRpcResponse(
2512
2513 testDevice->SendApplicationReadyRequest(
2514 connection->ArUuid(),
2515 connection->SessionKey());
2516
2517 EXPECT_NO_THROW(
2518 connection->ApplicationReady(1.0));
2519
2520 EXPECT_TRUE(
2521 testDevice->ReceivedApplicationReadyResponse());
2522
2523 const auto response =
2524 testDevice->ApplicationReadyResponse();
2525
2526 ASSERT_TRUE(
2527 response.has_value());
2528
2529 EXPECT_EQ(
2530 response->packetType,
2532
2533 EXPECT_EQ(
2534 response->operationNumber,
2536
2537 EXPECT_EQ(
2538 response->controlCommand,
2540}
2541
2550TEST_F(RpcConIntegrationTest, ApplicationReadyIgnoresOtherControlBlock)
2551{
2552 ASSERT_NO_THROW(
2553 Connect());
2554
2555 ASSERT_TRUE(
2556 testDevice->WaitForConnect());
2557
2558 testDevice->SendControlRequest(
2561 connection->ArUuid(),
2562 connection->SessionKey());
2563
2564 EXPECT_THROW(
2565 connection->ApplicationReady(
2566 std::chrono::milliseconds{50}.count() / 1000.0),
2568
2569 EXPECT_FALSE(
2570 testDevice->ReceivedApplicationReadyResponse());
2571}
2572
2582TEST_F(RpcConIntegrationTest, ApplicationReadyAcceptsUnexpectedControlCommand)
2583{
2584 ASSERT_NO_THROW(
2585 Connect());
2586
2587 ASSERT_TRUE(
2588 testDevice->WaitForConnect());
2589
2590 testDevice->SendControlRequest(
2592 0x1234,
2593 connection->ArUuid(),
2594 connection->SessionKey());
2595
2596 EXPECT_NO_THROW(
2597 connection->ApplicationReady(1.0));
2598
2599 EXPECT_TRUE(
2600 testDevice->ReceivedApplicationReadyResponse());
2601
2602 const auto response =
2603 testDevice->ApplicationReadyResponse();
2604
2605 ASSERT_TRUE(
2606 response.has_value());
2607
2608 EXPECT_EQ(
2609 response->blockType,
2611
2612 EXPECT_EQ(
2613 response->controlCommand,
2615
2616 EXPECT_EQ(
2617 response->arUuid,
2618 connection->ArUuid());
2619
2620 EXPECT_EQ(
2621 response->sessionKey,
2622 connection->SessionKey());
2623}
std::unique_ptr< RPCCon > connection
std::optional< dcp::DCPDeviceDescription > info
std::unique_ptr< TestRpcDevice > testDevice
static Bytes BuildApplicationReadyRequestPayload(const std::array< std::uint8_t, uuidLenght > &arUuid, std::uint16_t sessionKey)
std::atomic_bool sawRead
void HandleRpcFault(const PNRPCHeader &request, const asio::ip::udp::endpoint &sender)
PNRPCHeader SendRpcRequest(std::uint16_t operation, const Bytes &payload={}, std::optional< PNRPCHeader > packetTemplate=std::nullopt)
Send an arbitrary RPC REQUEST to the controller.
void HandleRead(const PNRPCHeader &request, const asio::ip::udp::endpoint &sender)
std::optional< PNRPCHeader > LastApplicationReadyRpcResponse() const
Return the raw RPC ApplicationReady response.
std::atomic_bool sawExpectedSubmodule
TestRpcDevice(ResponseMode mode)
Bytes LastWritePayload() const
std::atomic< std::size_t > requestCount
bool ReceivedApplicationReadyResponse() const
Check whether the simulated device received an ApplicationReady response.
std::optional< PNRPCHeader > lastApplicationReadyRequestRpc
void SendPacket(const PNRPCHeader &packet)
std::condition_variable stateCondition
bool SawExpectedSubmodule() const
void HandleConnectPnioError(const PNRPCHeader &request, const asio::ip::udp::endpoint &sender)
std::atomic_bool sawAlarmCr
std::atomic_bool sawExpectedConnect
asio::ip::udp::endpoint ControllerCControlEndpoint() const
std::atomic_bool sawInputIocr
std::optional< PNNRDData > LastApplicationReadyResponseNrd() const
Return the NRD contained in the ApplicationReady response.
void HandleRequest(const Bytes &bytes, const asio::ip::udp::endpoint &sender)
static std::optional< ControlRequest > ParseControlRequest(const PNRPCHeader &request)
std::uint16_t WriteIndex() const
void SendControlRequest(std::uint16_t blockType, std::uint16_t controlCommand, const std::array< std::uint8_t, uuidLenght > &arUuid, std::uint16_t sessionKey, std::uint16_t controlBlockProperties=0)
Send an arbitrary PROFINET CONTROL request.
std::atomic_bool sawOutputIocr
bool SawExpectedConnect() const
void HandleRpcReject(const PNRPCHeader &request, const asio::ip::udp::endpoint &sender)
asio::io_context ioContext
bool WaitForConnect(std::chrono::milliseconds timeout=std::chrono::milliseconds{1000})
Wait until the simulated device has received a valid CONNECT.
std::optional< ControlRequest > LastApplicationReadyRequest() const
Return the last ApplicationReady request sent by the device.
void HandleApplicationReadyResponse(const PNRPCHeader &response)
std::uint16_t ReadIndex() const
void HandleWrite(const PNRPCHeader &request, const asio::ip::udp::endpoint &sender)
void SendRpcResponse(const PNRPCHeader &response, const asio::ip::udp::endpoint &remote)
void SendApplicationReadyRequest(const std::array< std::uint8_t, uuidLenght > &arUuid, std::uint16_t sessionKey)
Send an ApplicationReady request from the simulated device.
std::atomic< std::uint16_t > writeIndex
std::optional< PNRPCHeader > LastApplicationReadyRpcRequest() const
Return the raw RPC ApplicationReady request.
asio::ip::udp::endpoint controllerEndpoint
std::atomic< std::uint16_t > readIndex
std::optional< ControlResponse > applicationReadyResponse
std::size_t RequestCount() const
std::atomic_bool sawWrite
std::optional< PNRPCHeader > lastApplicationReadyResponseRpc
std::atomic_bool running
void SendRpcResponse(std::uint16_t operation, const Bytes &payload={}, std::optional< PNRPCHeader > packetTemplate=std::nullopt)
Send an arbitrary RPC RESPONSE to the controller.
static std::optional< ControlResponse > ParseControlResponse(const PNRPCHeader &response)
std::optional< PNRPCHeader > lastConnectRequest
std::optional< ControlResponse > ApplicationReadyResponse() const
Return the parsed ApplicationReady response.
asio::ip::udp::socket socket
bool WaitForApplicationReadyResponse(std::chrono::milliseconds timeout=std::chrono::milliseconds{1000}) const
Wait until an ApplicationReady response was received.
static Bytes BuildControlRequestPayload(std::uint16_t blockType, std::uint16_t controlCommand, const std::array< std::uint8_t, uuidLenght > &arUuid, std::uint16_t sessionKey, std::uint16_t controlBlockProperties)
std::optional< ControlRequest > applicationReadyRequest
std::atomic_bool connected
void HandleConnect(const PNRPCHeader &request, const asio::ip::udp::endpoint &sender, bool sendRequestBeforeResponse, bool checkAlarmCr)
Failed to establish RPC connection.
Definition exceptions.h:615
RPC operation timed out.
Definition exceptions.h:587
Parsed PROFINET device information from a DCP response.
Definition dcp.h:268
std::string name
Station name.
Definition dcp.h:282
Connection to a PROFINET IO-Device: establishes an AR (Application Relationship) and performs acyclic...
Definition rpc.h:138
std::uint16_t SessionKey() const
The session key for this connection.
Definition rpc.h:428
bool HasAlarmCr() const override
Whether an AlarmCR was successfully established.
Definition rpc.h:453
const dcp::DCPDeviceDescription & Info() const override
The device this connection targets.
Definition rpc.h:414
void Disconnect() override
Send Release to terminate the AR.
Definition rpc.cpp:2101
void Close() noexcept override
Disconnect() and close the underlying sockets.
Definition rpc.cpp:2139
bool IsConnected() const override
Whether the AR is currently established.
Definition rpc.h:407
std::optional< ConnectResult > Connect(std::optional< MacAddress > srcMac=std::nullopt, bool withAlarmCr=false, std::optional< IOCRSetup > iocrSetup=std::nullopt) override
Establish (or re-establish) the AR with the device.
Definition rpc.cpp:1223
const std::array< std::uint8_t, uuidLenght > & ArUuid() const
The AR UUID for this connection.
Definition rpc.h:421
Declares exceptions and error types used by the PROFINET IO controller stack.
constexpr std::uint16_t BLOCK_IOD_CONTROL_APP_READY_REQ
Block type: IODControlReqAppReady.
Definition rpcTypes.h:65
constexpr std::uint16_t CONTROL_CMD_DONE
Control command bit: Done.
Definition rpcTypes.h:82
constexpr std::uint16_t BLOCK_IOD_CONTROL_PRM_END_REQ
Block type: IODControlReqPrmEnd.
Definition rpcTypes.h:63
constexpr std::uint16_t RPC_PORT
PROFINET IO RPC port (34964).
Definition rpcTypes.h:28
constexpr std::uint16_t CONTROL_CMD_PRM_END
Control command bit: PrmEnd.
Definition rpcTypes.h:76
constexpr std::uint16_t CONTROL_CMD_APP_READY
Control command bit: ApplicationReady.
Definition rpcTypes.h:78
BlockHeaderView PeekBlockHeader(const Bytes &data, std::size_t offset)
Definition rpc.h:54
constexpr std::uint16_t BLOCK_IOD_CONTROL_APP_READY_RES
Block type: IODControlResAppReady.
Definition rpcTypes.h:67
TEST_F(AlarmListenerTests, InitializesWithEndpoint)
constexpr int uuidLenght
Constant lenght of a UUID.
Definition util.h:40
constexpr int blockHeaderLength
Constant lenght of a blockHeader.
Definition protocol.h:58
std::array< std::uint8_t, macAddressLength > MacAddress
A 6-byte Ethernet MAC address.
Definition util.h:48
MacAddress String2Mac(const std::string &macStr)
Parse a colon-separated MAC address string.
Definition util.cpp:122
@ ApplicationReady
Application Ready command.
std::vector< std::uint8_t > Bytes
Generic byte buffer alias used throughout the library for raw wire data.
Definition protocol.h:55
constexpr std::size_t RECEIVE_BUFFER_LENGTH
Receive buffer length.
Definition util.h:43
PROFINET protocol wire-format structures and serialization helpers.
Bytes buffer
PROFINET DCE/RPC protocol structures and IO-Device connection API.
std::uint16_t blockType
std::uint16_t blockLength
std::array< std::uint8_t, uuidLenght > arUuid
std::array< std::uint8_t, uuidLenght > arUuid
Bytes ToBytes() const
Serialize this header back to raw bytes.
Definition protocol.h:1347
BlockType blockType
Block type identifier.
Definition protocol.h:1308
std::uint8_t blockVersionHigh
Block format major version.
Definition protocol.h:1317
std::uint8_t blockVersionLow
Block format minor version.
Definition protocol.h:1320
std::uint16_t blockLength
Length in bytes of the block body (excludes Type/Length fields).
Definition protocol.h:1311
IOCRBlockReq header: requests establishment of one IOCR (part of the CONNECT request).
Definition protocol.h:1984
static constexpr BlockType BLOCK_TYPE
Block type identifier for IOCRBlockReq.
Definition protocol.h:1987
std::uint16_t iocrType
IOCR type (Input/Output/MulticastProvider/MulticastConsumer).
Definition protocol.h:1994
static PNIOCRBlockReqHeader Parse(const Bytes &data)
Parse the fixed IOCRBlockReq header.
Definition protocol.h:2048
IOD read/write request/response header (RPC body for READ/WRITE/IMPLICIT_READ).
Definition protocol.h:1190
Bytes ToBytes() const
Serialize this header and payload back to raw bytes.
Definition protocol.h:1262
static PNIODHeader Parse(const Bytes &data)
Parse an IOD header and payload from raw bytes.
Definition protocol.h:1237
Bytes payload
Record data payload.
Definition protocol.h:1229
std::uint32_t length
Length in bytes of Payload.
Definition protocol.h:1220
std::uint16_t index
Record data index being read or written.
Definition protocol.h:1217
Control block used for Release/PrmBegin/PrmEnd/ApplicationReady/RTClass3 requests.
Definition protocol.h:1449
std::array< std::uint8_t, uuidLenght > arUuid
AR UUID this control operation applies to.
Definition protocol.h:1457
std::uint16_t controlBlockProperties
Control block property flags.
Definition protocol.h:1469
std::uint16_t padding1
Reserved/padding.
Definition protocol.h:1454
std::uint16_t sessionKey
Session key matching the AR.
Definition protocol.h:1460
std::array< std::uint8_t, blockHeaderLength > blockHeader
Nested 6-byte block header.
Definition protocol.h:1451
ControlCommand controlCommand
Control command bitmask (PrmEnd/AppReady/Release/Done/...).
Definition protocol.h:1466
std::uint16_t padding2
Reserved/padding.
Definition protocol.h:1463
Bytes ToBytes() const
Serialize this block back to raw bytes.
Definition protocol.h:1497
NRD (Network Representation Data) wrapper carrying the actual IOD payload.
Definition protocol.h:1128
Bytes payload
The wrapped IOD data.
Definition protocol.h:1145
static PNNRDData Parse(const Bytes &data)
Parse an NRD wrapper and payload from raw bytes.
Definition protocol.h:1153
Bytes ToBytes() const
Serialize this wrapper and payload back to raw bytes.
Definition protocol.h:1175
std::uint32_t argsLength
Length in bytes of the arguments (usually equals ActualCount).
Definition protocol.h:1133
std::uint32_t offset
Byte offset into the logical result (0 unless fragmented).
Definition protocol.h:1139
std::uint32_t argsMaximumStatus
Maximum status/argument buffer size accepted by the caller.
Definition protocol.h:1130
std::uint32_t maximumCount
Maximum number of bytes the caller can accept.
Definition protocol.h:1136
std::uint32_t actualCount
Actual length in bytes of Payload.
Definition protocol.h:1142
DCE/RPC PDU header used to carry PROFINET connect/read/write/control requests.
Definition protocol.h:860
Bytes ToBytes() const
Serialize this header and body back to raw bytes.
Definition protocol.h:1045
std::uint16_t operationNumber
Operation number (CONNECT/RELEASE/READ/WRITE/CONTROL/IMPLICIT_READ).
Definition protocol.h:955
std::uint16_t interfaceHint
Interface hint (0xFFFF if unused).
Definition protocol.h:958
static constexpr std::uint16_t READ
Operation number: READ.
Definition protocol.h:902
std::uint8_t flags1
Packet flags, first byte.
Definition protocol.h:925
static PNRPCHeader Parse(const Bytes &data)
Parse an RPC header and body from raw bytes.
Definition protocol.h:1012
std::uint8_t packetType
Packet type (REQUEST/RESPONSE/FAULT/...).
Definition protocol.h:922
static constexpr std::uint16_t WRITE
Operation number: WRITE.
Definition protocol.h:905
std::array< std::uint8_t, uuidLenght > interfaceUuid
Target interface UUID (identifies device/controller/supervisor role).
Definition protocol.h:940
std::uint32_t sequenceNumber
Sequence number within the activity.
Definition protocol.h:952
static constexpr std::uint8_t REJECT
Packet type: REJECT.
Definition protocol.h:881
static constexpr std::uint16_t CONNECT
Operation number: CONNECT.
Definition protocol.h:896
std::uint8_t version
DCE/RPC version (always 4 for PROFINET).
Definition protocol.h:919
static constexpr std::uint8_t REQUEST
Packet type: REQUEST.
Definition protocol.h:863
static constexpr std::uint16_t CONTROL
Operation number: CONTROL.
Definition protocol.h:908
static constexpr std::uint8_t RESPONSE
Packet type: RESPONSE.
Definition protocol.h:869
std::uint8_t flags2
Packet flags, second byte.
Definition protocol.h:928
Bytes payload
PDU body (NRD data for read/write/control, etc.).
Definition protocol.h:976
std::uint16_t activityHint
Activity hint (0xFFFF if unused).
Definition protocol.h:961
std::uint16_t lengthOfBody
Length in bytes of Payload.
Definition protocol.h:964
std::array< std::uint8_t, uuidLenght > objectUuid
Target object UUID.
Definition protocol.h:937
static constexpr std::uint8_t FAULT
Packet type: FAULT.
Definition protocol.h:872
std::array< std::uint8_t, uuidLenght > activityUuid
Activity (call) UUID correlating request/response pairs.
Definition protocol.h:943
Generic block header (type + length) used to scan response blocks.
Definition rpc.h:49
std::uint16_t blockType
Definition rpc.h:50
std::uint16_t blockLength
Definition rpc.h:51
Configuration for establishing cyclic IO alongside an AR.
Definition rpcTypes.h:149
std::vector< IOSlot > slots
Slots to include in the cyclic data frames.
Definition rpcTypes.h:151
One slot/subslot's expected module configuration and IO data sizes.
Definition rpcTypes.h:96
std::uint16_t slot
Slot number.
Definition rpcTypes.h:98
Configuration options for an RPC connection.
Definition rpcTypes.h:516
double timeoutSec
Timeout in seconds for RPC operations.
Definition rpcTypes.h:518
Bytes payload
Definition test_dcp.cpp:392
constexpr std::uint16_t kTestSubslot
Bytes ExtractNrdPayload(const PNRPCHeader &request)
bool ContainsBlockType(const Bytes &data, std::uint16_t blockType)
IOCRSetup MakeAumaLikeIocrSetup()
constexpr std::uint16_t kTestAlarmReference
constexpr std::uint16_t kTestRpcPort
PNRPCHeader MakeRpcReject(const PNRPCHeader &request)
constexpr std::uint32_t kTestApi
constexpr std::uint16_t kTestWriteIndex
std::size_t CountBlockType(const Bytes &data, std::uint16_t blockType)
std::vector< BlockView > FindBlocks(const Bytes &data)
Bytes MakeNrdResponse(const Bytes &payload)
Bytes GetBlock(const Bytes &data, const BlockView &block)
PNRPCHeader MakeRpcResponse(const PNRPCHeader &request, const Bytes &payload)
PNRPCHeader MakeRpcFault(const PNRPCHeader &request)
constexpr std::uint16_t kTestSlot
MacAddress TestMac()
constexpr std::uint16_t kTestReadIndex
dcp::DCPDeviceDescription MakeTestDeviceDescription()
Bytes MakePnioStatusResponse(std::uint32_t argsMaximumStatus, const Bytes &payload={})