12#include <gmock/gmock.h>
13#include <gtest/gtest.h>
30using ::testing::DoAll;
32using ::testing::Return;
33using ::testing::Throw;
42 0x00, 0x11, 0x22, 0x33, 0x44, 0x55};
44dcp::DCPDeviceDescription MakeInfo(
45 std::string name =
"dev",
46 std::string ip =
"192.168.0.10",
47 std::string mac =
"02:00:00:00:00:01")
49 dcp::DCPDeviceDescription info(
53 info.name = std::move(name);
54 info.ip = std::move(ip);
55 info.mac = std::move(mac);
56 info.deviceType =
"test";
57 info.netmask =
"255.255.255.0";
58 info.gateway =
"0.0.0.0";
76 const auto result =
ParseMac(
"02:00:00:00:00:01");
78 ASSERT_TRUE(result.has_value());
79 EXPECT_EQ(*result, (
MacAddress{0x02, 0x00, 0x00, 0x00, 0x00, 0x01}));
85 const auto result =
ParseMac(
"02-00-00-00-00-01");
87 ASSERT_TRUE(result.has_value());
88 EXPECT_EQ(*result, (
MacAddress{0x02, 0x00, 0x00, 0x00, 0x00, 0x01}));
94 const auto result =
ParseMac(
"0200.0000.0001");
96 ASSERT_TRUE(result.has_value());
97 EXPECT_EQ(*result, (
MacAddress{0x02, 0x00, 0x00, 0x00, 0x00, 0x01}));
103 EXPECT_FALSE(
ParseMac(
"not-a-mac").has_value());
139 EXPECT_TRUE(info.
OrderId().empty());
171 auto mock = std::make_unique<testing::NiceMock<rpc::MockRPCCon>>();
173 device = std::make_unique<ProfinetDevice>(MakeInfo(),
"eth0", kControllerMac, 5.0, std::move(mock));
186 EXPECT_CALL(*rpc, Read(0, 1, 2, 0xAFF0))
187 .WillOnce(Return(readResult));
190 EXPECT_EQ(device->Read(1, 2, 0xAFF0),
191 (
Bytes{0x01, 0x02}));
197 const Bytes data{0xAB};
199 EXPECT_CALL(*rpc, Write(3, 1, 2, 0xAFF1, data));
201 device->Write(1, 2, 0xAFF1, data, 3);
207 const std::vector<WriteItem> writes{
216 EXPECT_CALL(*rpc, Connect(_, _, _))
217 .WillOnce(Return(std::nullopt));
219 EXPECT_CALL(*rpc, WriteMultiple(_))
220 .WillOnce([](
const std::vector<rpc::WriteItem>& items)
222 EXPECT_EQ(items.size(), 1U);
223 if (items.size() != 1U)
225 return std::vector<blocks::WriteMultipleResult>{};
227 EXPECT_EQ(items[0].slot, 1);
228 EXPECT_EQ(items[0].subslot, 2);
229 EXPECT_EQ(items[0].index, 0xAFF1);
230 EXPECT_EQ(items[0].data, (
Bytes{0x01}));
231 EXPECT_EQ(items[0].api, 0);
233 return std::vector<blocks::WriteMultipleResult>{};
236 device->WriteMultiple(writes);
248 auto mock = std::make_unique<testing::NiceMock<rpc::MockRPCCon>>();
251 device = std::make_unique<ProfinetDevice>(MakeInfo(),
"eth0", kControllerMac, 5.0, std::move(mock));
262 EXPECT_CALL(*rpc, ReadIm0(1, 2))
263 .WillOnce(Return(expected));
265 EXPECT_EQ(device->ReadIm0(1, 2), expected);
272 EXPECT_CALL(*rpc, ReadIm1(1, 2))
273 .WillOnce(Return(expected));
275 EXPECT_EQ(device->ReadIm1(1, 2), expected);
283 EXPECT_CALL(*rpc, ReadIm2(1, 2))
284 .WillOnce(Return(expected));
286 EXPECT_EQ(device->ReadIm2(1, 2), expected);
293 EXPECT_CALL(*rpc, ReadIm3(1, 2))
294 .WillOnce(Return(expected));
296 EXPECT_EQ(device->ReadIm3(1, 2), expected);
303 EXPECT_CALL(*rpc, ReadIm4(1, 2))
304 .WillOnce(Return(expected));
306 EXPECT_EQ(device->ReadIm4(1, 2), expected);
313 EXPECT_CALL(*rpc, ReadIm5(1, 2))
314 .WillOnce(Return(expected));
316 EXPECT_EQ(device->ReadIm5(1, 2), expected);
323 EXPECT_CALL(*rpc, ReadAllIm(1, 2))
324 .WillOnce(Return(expected));
326 EXPECT_EQ(device->ReadAllIm(1, 2), expected);
332 EXPECT_CALL(*rpc, Write(0, 0, 1,
IM1, _))
333 .WillOnce([](std::uint32_t,
339 EXPECT_EQ(data.size(), 62U);
340 if (data.size() != 62U)
345 EXPECT_EQ(r.
U16(),
static_cast<std::uint16_t
>(0x0021));
346 EXPECT_EQ(r.
U16(),
static_cast<std::uint16_t
>(58));
348 Bytes expected(32,
static_cast<std::uint8_t
>(
' '));
349 std::ranges::copy(std::string_view{
"func"}, expected.begin());
351 EXPECT_EQ(
Bytes(data.begin() + 8, data.begin() + 40), expected);
356 device->WriteIm1(
"func",
"loc");
363 device->WriteIm1(std::string(33,
'x'),
"loc"),
364 std::invalid_argument);
371 device->WriteIm1(
"func", std::string(23,
'x')),
372 std::invalid_argument);
378 EXPECT_CALL(*rpc, Write(0, 0, 1,
IM2, _))
379 .WillOnce([](std::uint32_t,
385 EXPECT_EQ(data.size(), 24U);
386 if (data.size() != 24U)
392 EXPECT_EQ(r.
U16(), 0x0022);
393 EXPECT_EQ(r.
U16(), 20);
395 const Bytes expected{
396 '2',
'0',
'2',
'6',
'-',
'0',
'7',
'-',
'2',
'1',
397 ' ',
'1',
'8',
':',
'0',
'0'};
399 EXPECT_EQ(
Bytes(data.begin() + 8, data.begin() + 24),
403 device->WriteIm2(
"2026-07-21 18:00");
410 device->WriteIm2(std::string(17,
'x')),
411 std::invalid_argument);
417 EXPECT_CALL(*rpc, Write(0, 0, 1,
IM3, _))
418 .WillOnce([](std::uint32_t,
424 ASSERT_EQ(data.size(), 62U);
425 if (data.size() != 62U)
431 EXPECT_EQ(r.
U16(), 0x0023);
432 EXPECT_EQ(r.
U16(), 58);
433 Bytes expected(54,
static_cast<std::uint8_t
>(
' '));
434 std::ranges::copy(
"descriptor" | std::views::take(10), expected.begin());
436 EXPECT_EQ(
Bytes(data.begin() + 8, data.begin() + 62), expected);
439 device->WriteIm3(
"descriptor");
446 device->WriteIm3(std::string(55,
'x')),
447 std::invalid_argument);
459 auto mock = std::make_unique<testing::NiceMock<rpc::MockRPCCon>>();
462 device = std::make_unique<ProfinetDevice>(MakeInfo(),
"eth0", kControllerMac, 5.0, std::move(mock));
465 std::uint16_t blockLength,
466 std::uint8_t verHi = 1,
467 std::uint8_t verLo = 0)
478 std::uint16_t subslot,
479 std::uint16_t moduleState,
480 std::uint16_t submoduleState)
497 static_cast<std::uint16_t
>(body.size() + 2));
500 out.insert(out.end(), body.begin(), body.end());
512 const Bytes data = MakeModuleDiffBlock(
520 EXPECT_CALL(*rpc, ReadModuleDiff()).WillOnce(Return(expected));
521 EXPECT_EQ(device->ReadModuleDiff(), expected);
528 EXPECT_CALL(*rpc, ReadDiagnosis(1, 2, 0xF00C))
529 .WillOnce(Return(expected));
531 EXPECT_EQ(device->ReadDiagnosis(1, 2, 0xF00C), expected);
537 std::map<std::uint16_t, diagnosis::DiagnosisData> expected;
538 EXPECT_CALL(*rpc, ReadAllDiagnosis()).WillOnce(Return(expected));
539 EXPECT_EQ(device->ReadAllDiagnosis(), expected);
545 std::vector<blocks::SlotInfo> expected;
546 EXPECT_CALL(*rpc, DiscoverSlots()).WillOnce(Return(expected));
547 EXPECT_EQ(device->DiscoverSlots(), expected);
554 EXPECT_CALL(*rpc, ReadPdRealData()).WillOnce(Return(expected));
555 EXPECT_EQ(device->ReadTopology(), expected);
567 auto mock = std::make_unique<testing::NiceMock<rpc::MockRPCCon>>();
570 device = std::make_unique<ProfinetDevice>(MakeInfo(),
"eth0", kControllerMac, 5.0, std::move(mock));
580 EXPECT_CALL(*rpc, Read(0, 0, 0, 0x800C))
583 EXPECT_FALSE(device->ReadAlarm().has_value());
592 EXPECT_CALL(*rpc, Read(_, _, _, _))
593 .WillOnce(Return(readResult));
595 EXPECT_FALSE(device->ReadAlarm().has_value());
610 0x00, 0x00, 0x00, 0x00,
613 0x00, 0x00, 0x00, 0x01,
614 0x00, 0x00, 0x00, 0x01,
622 EXPECT_CALL(*rpc, Read(_, _, _, _))
623 .WillOnce(Return(readResult));
625 const auto alarm = device->ReadAlarm(1, 0);
627 ASSERT_TRUE(alarm.has_value());
628 EXPECT_EQ(alarm->slotNumber, 1);
640 auto mockRPC = std::make_unique<testing::NiceMock<rpc::MockRPCCon>>();
643 auto mockAL = std::make_unique<testing::NiceMock<MockAlarmListener>>();
645 device = std::make_unique<ProfinetDevice>(MakeInfo(),
"eth0", kControllerMac, 5.0, std::move(mockRPC), std::move(mockAL));
656 EXPECT_CALL(*rpc, Connect);
660 EXPECT_TRUE(device->IsConnected());
666 EXPECT_CALL(*rpc, Connect)
667 .WillOnce(Throw(
RPCError(
"refused")));
670 EXPECT_FALSE(device->IsConnected());
680 EXPECT_CALL(*rpc, Connect).Times(0);
684 EXPECT_TRUE(device->IsConnected());
690 EXPECT_CALL(*rpc, Disconnect());
692 device->Disconnect();
694 EXPECT_FALSE(device->IsConnected());
700 EXPECT_CALL(*rpc, Close());
704 EXPECT_FALSE(device->IsConnected());
714 EXPECT_CALL(*alarmListener,
Stop());
749 auto mockRPC = std::make_unique<testing::NiceMock<rpc::MockRPCCon>>();
752 auto mockAL = std::make_unique<testing::NiceMock<MockAlarmListener>>();
754 device = std::make_unique<ProfinetDevice>(MakeInfo(),
"eth0", kControllerMac, 5.0, std::move(mockRPC), std::move(mockAL));
765 EXPECT_THROW(device->StartAlarmListener(), std::runtime_error);
773 EXPECT_THROW(device->StartAlarmListener(), std::runtime_error);
779 EXPECT_CALL(*rpc, HasAlarmCr())
780 .WillOnce(Return(
true));
781 EXPECT_CALL(*rpc, LocalAlarmRef())
782 .WillOnce(Return(1));
783 EXPECT_CALL(*rpc, DeviceAlarmRef())
784 .WillOnce(Return(42));
786 EXPECT_CALL(*alarmListener,
Start(_, _)).Times(1);
787 EXPECT_CALL(*alarmListener, IsRunning())
788 .WillOnce(Return(
false))
789 .WillOnce(Return(
false))
790 .WillOnce(Return(
true));
791 EXPECT_CALL(*rpc, Connect);
794 device->StartAlarmListener();
796 EXPECT_TRUE(device->AlarmListenerRunning());
806 EXPECT_CALL(*alarmListener, AddAlarmNotificationCallback(_));
807 EXPECT_CALL(*alarmListener, IsRunning())
808 .WillOnce(Return(
true));
809 device->OnAlarm(callback);
815 device->StopAlarmListener();
817 EXPECT_FALSE(device->AlarmListenerRunning());
822 EXPECT_CALL(*rpc, HasAlarmCr())
823 .WillRepeatedly(Return(
true));
825 EXPECT_CALL(*rpc, LocalAlarmRef())
826 .WillRepeatedly(Return(1));
828 EXPECT_CALL(*rpc, DeviceAlarmRef())
829 .WillRepeatedly(Return(42));
831 EXPECT_CALL(*alarmListener, IsRunning())
832 .WillOnce(Return(
false))
833 .WillOnce(Return(
false))
834 .WillRepeatedly(Return(
true));
838 EXPECT_CALL(*alarmListener, AddAlarmNotificationCallback(_))
841 EXPECT_CALL(*alarmListener,
Start(_, _))
844 EXPECT_CALL(*rpc, Connect);
847 device->OnAlarm(callback);
849 device->StartAlarmListener();
850 device->StartAlarmListener();
856 EXPECT_CALL(*rpc, HasAlarmCr())
857 .WillOnce(Return(
true));
859 EXPECT_CALL(*rpc, LocalAlarmRef())
860 .WillOnce(Return(1));
862 EXPECT_CALL(*rpc, DeviceAlarmRef())
863 .WillOnce(Return(42));
865 EXPECT_CALL(*alarmListener, IsRunning())
866 .WillOnce(Return(
false))
867 .WillOnce(Return(
false));
869 EXPECT_CALL(*alarmListener, AddAlarmNotificationCallback(_))
872 EXPECT_CALL(*alarmListener,
Start(_, _))
875 EXPECT_CALL(*rpc, Connect);
880 device->StartAlarmListener();
887 EXPECT_CALL(*alarmListener, IsRunning())
888 .WillOnce(Return(
true));
890 EXPECT_CALL(*alarmListener, AddAlarmNotificationCallback(_))
893 device->OnAlarm(callback);
904 auto mock = std::make_unique<testing::NiceMock<rpc::MockRPCCon>>();
907 device = std::make_unique<ProfinetDevice>(MakeInfo(),
"eth0", kControllerMac, 5.0, std::move(mock));
918 EXPECT_CALL(*rpc, ReadIm0(0, 1))
919 .WillOnce(Return(expectedIm0));
923 ASSERT_TRUE(info.
im0.has_value());
929 EXPECT_CALL(*rpc, ReadIm0(0, 1))
930 .WillOnce(Throw(
RPCError(
"unsupported")));
934 EXPECT_FALSE(info.
im0.has_value());
944 EXPECT_CALL(*rpc, ReadPdRealData())
945 .WillOnce(Return(expectedTopology));
947 const DeviceInfo info = device->GetInfo(
true);
949 ASSERT_TRUE(info.
topology.has_value());
955 EXPECT_CALL(*rpc, ReadPdRealData()).Times(0);
959 EXPECT_FALSE(info.
topology.has_value());
980 "ProfinetDevice('dev', 192.168.0.10, disconnected)");
Google Mock implementation of IAlarmListener.
Failed to establish RPC connection.
MockAlarmListener * alarmListener
std::unique_ptr< ProfinetDevice > device
std::unique_ptr< ProfinetDevice > device
MockAlarmListener * alarmListener
std::unique_ptr< ProfinetDevice > device
Bytes MakeModuleDiffBlock(std::uint16_t slot, std::uint16_t subslot, std::uint16_t moduleState, std::uint16_t submoduleState)
Bytes MakeBlockHeader(std::uint16_t blockType, std::uint16_t blockLength, std::uint8_t verHi=1, std::uint8_t verLo=0)
std::unique_ptr< ProfinetDevice > device
std::unique_ptr< ProfinetDevice > device
std::unique_ptr< ProfinetDevice > device
std::unique_ptr< ProfinetDevice > device
High-level PROFINET device interface.
std::string ToString() const
Human-readable summary of this device, for logging/debugging.
Google Mock implementation of the IRPCCon RPC connection interface.
Lightweight cursor for parsing a big-endian byte buffer without copies.
std::uint16_t U16()
Read and consume a big-endian 16-bit value.
High-level PROFINET device interface and device lifecycle management.
Declares exceptions and error types used by the PROFINET IO controller stack.
PROFINET record-data indices, block types, and protocol enumerations.
Google Mock implementation of the PROFINET alarm listener interface.
Google Mock implementation of the IRPCCon PROFINET RPC connection interface.
ModuleDiffBlock ParseModuleDiffBlock(const Bytes &data)
Parse a ModuleDiffBlock (0x8104) from raw bytes.
bool IsMacAddress(const std::string &identifier)
Whether a string parses as a MAC address.
TEST_F(DeviceTest, ParseMacAcceptsColonDashAndDotSeparators)
std::optional< MacAddress > ParseMac(const std::string &macStr)
Parse a MAC address string in any common separator style.
constexpr MacAddress kControllerMac
void PutU8(std::vector< std::uint8_t > &out, std::uint8_t value)
Append a single byte to a buffer.
void PutU16(std::vector< std::uint8_t > &out, std::uint16_t value)
Append a 16-bit value to a buffer in big-endian order.
void PutU32(std::vector< std::uint8_t > &out, std::uint32_t value)
Append a 32-bit value to a buffer in big-endian order.
@ ModuleDifference
Module difference block.
std::array< std::uint8_t, macAddressLength > MacAddress
A 6-byte Ethernet MAC address.
MacAddress String2Mac(const std::string &macStr)
Parse a colon-separated MAC address string.
constexpr std::uint16_t IM2
I&M2.
constexpr std::uint16_t IM1
I&M1.
constexpr std::uint16_t MODULE_STATE_PROPER_MODULE
Module State Proper Module.
constexpr std::uint16_t SUBMODULE_STATE_OK
Submodule State Ok.
std::vector< std::uint8_t > Bytes
Generic byte buffer alias used throughout the library for raw wire data.
constexpr std::uint16_t IM3
I&M3.
Complete parsed AlarmNotification PDU: header fields + parsed items.
I&M0: mandatory identification data (vendor, order ID, serial number, revisions).
I&M1: user-assigned tag function and location strings.
I&M3: free-text descriptor.
I&M4: PROFIsafe signature (binary, not text).
I&M5: free-text annotation.
Parsed PDRealData (0xF841) structure.
Complete device information summary.
std::string SerialNumber() const
Serial number from I&M0, trimmed.
std::optional< PNInM0 > im0
I&M0 identification data, if it was read successfully.
std::uint16_t HardwareRevision() const
Hardware revision from I&M0.
std::string OrderId() const
Order ID from I&M0, trimmed.
std::string SoftwareRevision() const
Software revision string from I&M0.
std::optional< blocks::PDRealData > topology
Physical topology, if it was read successfully.
A single write operation for ProfinetDevice::WriteMultiple().
std::uint16_t slot
Slot number.
Complete diagnosis data for one slot/subslot.
All I&M records a device supports, read in one call.