24std::string Trim(
const std::string& s)
26 const std::size_t start = s.find_first_not_of(
" \t\r\n\0", 0, 4);
27 if (start == std::string::npos)
31 const std::size_t end = s.find_last_not_of(
" \t\r\n\0", std::string::npos, 4);
32 return s.substr(start, end - start + 1);
36Bytes BuildImBlockHeader(std::uint16_t blockType, std::uint16_t blockLength)
48Bytes Latin1Pad(
const std::string& s, std::size_t maxLen)
50 Bytes out(s.begin(), s.begin() + std::min(s.size(), maxLen));
51 out.resize(maxLen,
static_cast<std::uint8_t
>(
' '));
57std::optional<MacAddress>
ParseMac(
const std::string& macStrIn)
59 std::string s = macStrIn;
60 std::ranges::transform(s, s.begin(), [](
unsigned char c)
61 { return std::tolower(c); });
65 if (s.find(
'.') != std::string::npos)
67 std::vector<std::string> groups;
68 std::stringstream ss(s);
71 while (std::getline(ss, group,
'.'))
73 groups.push_back(group);
77 if (groups.size() == 3)
79 if (!std::ranges::all_of(groups, [](
const std::string& group)
81 return group.size() == 4 &&
82 std::ranges::all_of(group, [](
unsigned char c)
84 return std::isxdigit(c) != 0;
93 for (std::size_t i = 0; i < 3; ++i)
96 static_cast<std::uint16_t
>(
97 std::stoul(groups[i],
nullptr, 16));
100 static_cast<std::uint8_t
>(value >> 8);
103 static_cast<std::uint8_t
>(value & 0xff);
111 if (groups.size() == 6)
113 if (!std::ranges::all_of(groups, [](
const std::string& group)
115 return group.size() == 2 &&
116 std::ranges::all_of(group, [](
unsigned char c)
118 return std::isxdigit(c) != 0;
127 for (std::size_t i = 0; i < 6; ++i)
129 mac[i] =
static_cast<std::uint8_t
>(
130 std::stoul(groups[i],
nullptr, 16));
139 std::ranges::replace(s,
'-',
':');
141 std::vector<std::string> parts;
142 std::stringstream ss(s);
144 while (std::getline(ss, part,
':'))
146 parts.push_back(part);
157 if (parts[i].empty() || parts[i].size() > 2)
161 for (
const char c : parts[i])
163 if (std::isxdigit(
static_cast<unsigned char>(c)) == 0)
168 mac[i] =
static_cast<std::uint8_t
>(std::stoul(parts[i],
nullptr, 16));
175 return ParseMac(identifier).has_value();
184 return im0 ? Trim(
im0->imSerialNumber) :
"";
188 return im0 ? Trim(
im0->orderId) :
"";
192 return im0 ?
im0->imHardwareRevision : 0;
202 const std::string prefixStr = (
im0->swRevisionPrefix != 0U)
203 ? std::string(1,
static_cast<char>(
im0->swRevisionPrefix))
207 const std::string result = std::format(
"{}{}.{}.{}",
209 im0->imSwRevisionFunctionalEnhancement,
210 im0->imSwRevisionBugFix,
211 im0->imSwRevisionInternalChange);
221 double timeoutSec, std::unique_ptr<rpc::IRPCCon> rpc, std::unique_ptr<IAlarmListener> alarmListener)
222 : info(std::move(info)), interface(std::move(interface)), srcMac(srcMac), timeout(timeoutSec), rpc(std::move(rpc)), alarmListener(std::move(alarmListener))
226 : info(std::move(other.info)),
227 interface(std::move(other.interface)),
228 srcMac(other.srcMac),
229 timeout(other.timeout),
230 rpc(std::move(other.rpc)),
231 connected(std::exchange(other.connected,
false)),
232 alarmListener(std::move(other.alarmListener)),
233 alarmCallbacks(std::move(other.alarmCallbacks))
243 info = std::move(other.info);
244 interface = std::move(other.interface);
245 srcMac = other.srcMac;
246 timeout = other.timeout;
248 rpc = std::move(other.rpc);
249 connected = std::exchange(other.connected,
false);
250 alarmListener = std::move(other.alarmListener);
251 alarmCallbacks = std::move(other.alarmCallbacks);
276 auto targetMac =
ParseMac(identifier);
281 for (
const auto& [
Mac, Blocks] : responses)
283 if (
Mac == *targetMac)
296 static_cast<int>(timeoutSec));
299 std::cout << device.ToString() <<
"\n\n";
320 for (
const auto& [
Mac, Blocks] : responses)
350 rpc = std::make_unique<rpc::RPCCon>(
info, options,
nullptr,
nullptr, &
ioContext);
414 catch (
const std::exception& e)
423 if (this->
rpc.get() == &releaseRpc)
457 returnInfo.
im0 = conn.ReadIm0();
466 auto endpoints = conn.EpmLookup(returnInfo.
ip);
467 for (
const auto& ep : endpoints)
469 if (!ep.annotation.empty())
476 catch (
const std::exception&)
484 returnInfo.
topology = conn.ReadPdRealData();
500 auto responseIOD = conn.Read(api, slot, subslot, index);
501 return responseIOD.payload;
516 return rpc->ReadImplicit(api, slot, subslot, index).payload;
541 conn.Write(api, slot, subslot, index, data);
547 std::vector<rpc::WriteItem> items;
548 items.reserve(writes.size());
549 for (
const auto& w : writes)
551 items.push_back({.slot = w.slot, .subslot = w.subslot, .index = w.index, .data = w.data, .api = w.api});
553 return conn.WriteMultiple(items);
585void ProfinetDevice::WriteIm1(
const std::string& tagFunction,
const std::string& tagLocation, std::uint16_t slot, std::uint16_t subslot)
587 if (tagFunction.size() > 32)
589 throw std::invalid_argument(
"tag_function exceeds 32 character limit");
591 if (tagLocation.size() > 22)
593 throw std::invalid_argument(
"tag_location exceeds 22 character limit");
596 Bytes data = BuildImBlockHeader(0x0021, 58);
597 data.push_back(0x00);
598 data.push_back(0x00);
599 Bytes func = Latin1Pad(tagFunction, 32);
600 Bytes loc = Latin1Pad(tagLocation, 22);
601 data.insert(data.end(), func.begin(), func.end());
602 data.insert(data.end(), loc.begin(), loc.end());
609 if (date.size() > 16)
611 throw std::invalid_argument(
"date exceeds 16 character limit");
614 Bytes data = BuildImBlockHeader(0x0022, 20);
615 data.push_back(0x00);
616 data.push_back(0x00);
617 Bytes dateBytes = Latin1Pad(date, 16);
618 data.insert(data.end(), dateBytes.begin(), dateBytes.end());
625 if (descriptor.size() > 54)
627 throw std::invalid_argument(
"descriptor exceeds 54 character limit");
630 Bytes data = BuildImBlockHeader(0x0023, 58);
631 data.push_back(0x00);
632 data.push_back(0x00);
633 Bytes descBytes = Latin1Pad(descriptor, 54);
634 data.insert(data.end(), descBytes.begin(), descBytes.end());
670 const Bytes data =
Read(slot, subslot, index);
671 if (data.size() >= 26)
697 throw std::runtime_error(
"Must be connected first");
700 if (!
rpc->HasAlarmCr())
702 throw std::runtime_error(
"AlarmCR not established. Reconnect with with_alarm_cr=true via RPCCon directly");
713 endpoint.
deviceRef =
static_cast<std::uint16_t
>(
rpc->DeviceAlarmRef());
744 int maxConsecutiveTimeouts,
745 std::unique_ptr<cyclic::ICyclicController>
controller)
749 auto result = conn.Connect(
srcMac,
true, iocrSetup);
750 if (!result || !result->HasCyclic())
752 throw std::runtime_error(
"Cyclic IO not established by device");
756 conn.ApplicationReady(30.0);
763 controller = std::make_unique<cyclic::CyclicController>(
765 input_iocr, output_iocr, maxConsecutiveTimeouts);
772 std::uint16_t subslot)
799std::vector<ProfinetDevice>
Scan(
const std::string& interface,
double timeoutSec,
800 std::unique_ptr<IDcpScanner> dcpScanner, std::unique_ptr<IMacAddressProvider> macAddressProvider)
802 if (dcpScanner ==
nullptr)
804 dcpScanner = std::make_unique<DcpScanner>();
806 if (macAddressProvider ==
nullptr)
808 macAddressProvider = std::make_unique<MacAddressProvider>();
813 const MacAddress srcMac = macAddressProvider->GetMac(interface);
825 auto responses = dcpScanner->Scan(interface, timeoutSec);
827 std::vector<ProfinetDevice> devices;
828 devices.reserve(responses.size());
829 for (
const auto& [
Mac, Blocks] : responses)
832 devices.emplace_back(std::move(info), interface, srcMac, timeoutSec);
837std::map<std::string, DeviceInfo>
ScanDict(
const std::string& interface,
double timeoutSec, std::unique_ptr<IDcpScanner> dcpScanner, std::unique_ptr<IMacAddressProvider> macAddressProvider)
839 std::map<std::string, DeviceInfo> result;
840 for (
auto& dev :
Scan(interface, timeoutSec, std::move(dcpScanner), std::move(macAddressProvider)))
843 info.
name = dev.Name();
845 info.
mac = dev.Mac();
846 result[dev.Mac()] = info;
Asio-backed platform-independent raw Ethernet transport.
Platform-independent raw Ethernet transport using Asio.
Keeps an Asio I/O context running on a dedicated worker thread.
Device not found via DCP.
Failed to establish RPC connection.
Parsed PROFINET device information from a DCP response.
std::string netmask
IPv4 subnet mask, or "0.0.0.0" if unset.
std::string ToString() const
Multi-line human-readable summary of this device.
std::string name
Station name.
std::uint16_t VendorId() const
Combine VendorHigh/VendorLow into a 16-bit vendor ID.
std::string gateway
IPv4 default gateway, or "0.0.0.0" if unset.
std::uint16_t DeviceId() const
Combine DeviceHigh/DeviceLow into a 16-bit device ID.
std::string VendorName() const
Look up the vendor name for this device's vendor ID.
std::string mac
Device's MAC address, formatted as a string.
std::string ip
IPv4 address, or "0.0.0.0" if unset.
std::vector< std::string > deviceRoles
Decoded device role names (see DecodeDeviceRole()).
std::string deviceType
Device type string (e.g. model name).
dcp::ResponseMap Scan(const std::string &interface, double timeoutSec) override
Discover PROFINET devices using DCP.
High-level PROFINET device interface.
std::string ToString() const
Human-readable summary of this device, for logging/debugging.
PNInM0 ReadIm0(std::uint16_t slot=0, std::uint16_t subslot=1)
Read I&M0 (mandatory identification data).
std::string Mac() const
The device's MAC address.
rpc::AllIM ReadAllIm(std::uint16_t slot=0, std::uint16_t subslot=1)
Read all I&M records the device supports.
void ReleaseAr(rpc::IRPCCon &releaseRpc)
Release a PROFINET Application Relationship.
blocks::ModuleDiffBlock ReadModuleDiff()
Read and parse the device's ModuleDiffBlock.
AsioIoContextRunner ioContextRunner
Keeps the Asio execution context running.
~ProfinetDevice()
Close() the connection and release resources.
static ProfinetDevice Discover(const std::string &identifier, const std::string &interface, double timeoutSec=10.0)
Discover a device by station name or MAC address.
std::map< std::uint16_t, rpc::IndexProbeResult > EnumerateIndices(std::uint16_t slot=0, std::uint16_t subslot=1)
Probe standard indices and report which are readable/empty/erroring.
std::vector< blocks::WriteMultipleResult > WriteMultiple(const std::vector< WriteItem > &writes)
Write multiple records atomically.
void Close() noexcept
Stop the alarm listener (if running) and fully close the RPC connection.
PNInM5 ReadIm5(std::uint16_t slot=0, std::uint16_t subslot=1)
Read I&M5 (free-text annotation).
std::string interface
Network interface name in use.
void StopAlarmListener()
Stop the alarm listener.
PNInM4 ReadIm4(std::uint16_t slot=0, std::uint16_t subslot=1)
Read I&M4 (PROFIsafe signature).
blocks::PDRealData ReadTopology()
Read the device's physical topology.
void StartAlarmListener()
Start the background alarm listener.
void WriteIm1(const std::string &tagFunction, const std::string &tagLocation, std::uint16_t slot=0, std::uint16_t subslot=1)
Write I&M1 (tag function + location).
std::unique_ptr< IAlarmListener > alarmListener
Background alarm listener, created lazily by StartAlarmListener().
void OnAlarm(std::function< void(const AlarmNotification &)> callback)
Register a callback for alarm notifications.
void WriteIm3(const std::string &descriptor, std::uint16_t slot=0, std::uint16_t subslot=1)
Write I&M3 (free-text descriptor).
void Write(std::uint16_t slot, std::uint16_t subslot, std::uint16_t index, const Bytes &data, std::uint32_t api=0)
Write a raw record.
PNInM2 ReadIm2(std::uint16_t slot=0, std::uint16_t subslot=1)
Read I&M2 (installation date).
Bytes ReadImplicit(std::uint16_t slot, std::uint16_t subslot, std::uint16_t index, std::uint32_t api=0)
Read a record using the AR-less Read Implicit service.
std::unique_ptr< rpc::IRPCCon > rpc
Underlying RPC connection, created lazily on first use.
bool connected
Whether the AR is currently established.
asio::io_context ioContext
Asio execution context.
ProfinetDevice & operator=(const ProfinetDevice &)=delete
Not copyable (owns an RPC connection and alarm listener).
PNInM1 ReadIm1(std::uint16_t slot=0, std::uint16_t subslot=1)
Read I&M1 (tag function/location).
diagnosis::DiagnosisData ReadDiagnosis(std::uint16_t slot=0, std::uint16_t subslot=0, std::uint16_t index=0xF000)
Read and parse diagnosis data at the given location/index.
void Connect()
Establish the AR.
Bytes Read(std::uint16_t slot, std::uint16_t subslot, std::uint16_t index, std::uint32_t api=0)
Read a raw record.
void WriteIm2(const std::string &date, std::uint16_t slot=0, std::uint16_t subslot=1)
Write I&M2 (installation date).
ProfinetDevice(dcp::DCPDeviceDescription info, std::string interface, MacAddress srcMac, double timeoutSec=5.0, std::unique_ptr< rpc::IRPCCon > rpc=nullptr, std::unique_ptr< IAlarmListener > alarmListener=nullptr)
Construct a device wrapper directly.
static ProfinetDevice FromDcpInfo(dcp::DCPDeviceDescription info, const std::string &interface, double timeoutSec=5.0)
Wrap an already-discovered DCPDeviceDescription.
std::unique_ptr< cyclic::ICyclicController > StartCyclic(const rpc::IOCRSetup &iocrSetup, int maxConsecutiveTimeouts=3, std::unique_ptr< cyclic::ICyclicController > controller=nullptr)
Establish cyclic IO end-to-end.
std::vector< blocks::SlotInfo > DiscoverSlots()
List the device's populated slots.
PNInM3 ReadIm3(std::uint16_t slot=0, std::uint16_t subslot=1)
Read I&M3 (free-text descriptor).
std::optional< AlarmNotification > ReadAlarm(std::uint16_t slot=0, std::uint16_t subslot=0, std::uint16_t index=0x800C)
Attempt to read an alarm notification at the given location.
MacAddress srcMac
This host's MAC address.
static ProfinetDevice FromIp(const std::string &ip, const std::string &interface, double timeoutSec=10.0)
Discover a device by IP address.
void Disconnect()
Gracefully disconnect (sends Release).
bool AlarmListenerRunning() const
Whether the alarm listener is currently running.
double timeout
Default RPC response timeout in seconds.
std::vector< std::function< void(const AlarmNotification &)> > alarmCallbacks
Callbacks registered via OnAlarm(), applied to alarmListener once started.
rpc::IRPCCon & EnsureConnected()
Return the underlying RPCCon, connecting first if necessary.
DeviceInfo GetInfo(bool includeTopology=false)
Gather a complete device information summary.
dcp::DCPDeviceDescription info
DCP-discovered description of this device.
std::map< std::uint16_t, diagnosis::DiagnosisData > ReadAllDiagnosis()
Read diagnosis from all standard diagnosis indices.
Abstract interface for a PROFINET RPC connection.
virtual PNInM5 ReadIm5(std::uint16_t slot=0, std::uint16_t subslot=1)=0
Reads I&M5 free-text annotation data.
virtual diagnosis::DiagnosisData ReadDiagnosis(std::uint16_t slot=0, std::uint16_t subslot=0, std::uint16_t index=0xF000)=0
Reads diagnosis information.
virtual PNInM2 ReadIm2(std::uint16_t slot=0, std::uint16_t subslot=1)=0
Reads I&M2 installation date data.
virtual AllIM ReadAllIm(std::uint16_t slot=0, std::uint16_t subslot=1)=0
Reads all supported I&M records.
virtual PNInM3 ReadIm3(std::uint16_t slot=0, std::uint16_t subslot=1)=0
Reads I&M3 free-text descriptor data.
virtual PNInM4 ReadIm4(std::uint16_t slot=0, std::uint16_t subslot=1)=0
Reads I&M4 PROFIsafe signature data.
virtual PNInM1 ReadIm1(std::uint16_t slot=0, std::uint16_t subslot=1)=0
Reads I&M1 tag function and location data.
virtual void Close() noexcept=0
Disconnects and closes the underlying RPC sockets.
virtual void Disconnect()=0
Disconnects the Application Relationship.
virtual PNInM0 ReadIm0(std::uint16_t slot=0, std::uint16_t subslot=1)=0
Reads I&M0 identification data.
virtual std::map< std::uint16_t, diagnosis::DiagnosisData > ReadAllDiagnosis()=0
Reads diagnosis information for all standard indices.
virtual blocks::ModuleDiffBlock ReadModuleDiff()=0
Reads the ModuleDiffBlock.
virtual blocks::PDRealData ReadPdRealData()=0
Reads physical-device topology information.
virtual std::map< std::uint16_t, IndexProbeResult > EnumerateIndices(std::uint16_t slot=0, std::uint16_t subslot=1, std::optional< std::vector< std::uint16_t > > customIndices=std::nullopt)=0
Enumerates readable PROFINET record indices.
virtual std::vector< blocks::SlotInfo > DiscoverSlots()=0
Discovers the populated device slots.
Connection to a PROFINET IO-Device: establishes an AR (Application Relationship) and performs acyclic...
void Close() noexcept override
Disconnect() and close the underlying sockets.
PNIODHeader ReadImplicit(std::uint32_t api, std::uint16_t slot, std::uint16_t subslot, std::uint16_t idx) override
Read a data record without an established AR.
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.
void SendDiscover(IRawEthernetSocket &sock, const MacAddress &src, std::uint16_t responseDelay=DEFAULT_RESPONSE_DELAY_FACTOR)
Send a DCP Identify multicast request to discover all devices on the segment.
std::map< MacAddress, std::map< BlockKey, std::vector< std::uint8_t > > > ResponseMap
Result of ReadResponse(): MAC address -> parsed (option,suboption) -> payload blocks.
ResponseMap ReadResponse(IRawEthernetSocket &sock, const MacAddress &myMac, double timeoutSec=DEFAULT_RESPONSE_TIMEOUT, bool once=false, std::optional< std::uint32_t > expectedXid=std::nullopt)
Read and parse DCP Identify responses.
bool IsMacAddress(const std::string &identifier)
Whether a string parses as a MAC address.
std::vector< ProfinetDevice > Scan(const std::string &interface="eth0", double timeoutSec=3.0, std::unique_ptr< IDcpScanner > dcpScanner=nullptr, std::unique_ptr< IMacAddressProvider > macAddressProvider=nullptr)
Scan the network for PROFINET devices via DCP Identify.
std::optional< MacAddress > ParseMac(const std::string &macStr)
Parse a MAC address string in any common separator style.
std::map< std::string, DeviceInfo > ScanDict(const std::string &interface="eth0", double timeoutSec=3.0, std::unique_ptr< IDcpScanner > dcpScanner=nullptr, std::unique_ptr< IMacAddressProvider > macAddressProvider=nullptr)
Scan the network and return a name -> DeviceInfo map.
dcp::DCPDeviceDescription GetStationInfo(IRawEthernetSocket &sock, const MacAddress &src, const std::string &name, double timeoutSec=DEFAULT_TIMEOUT *2)
Resolve a device by PROFINET station name via DCP.
std::pair< IOCRConfig, IOCRConfig > BuildIocrConfigs(const std::vector< SlotT > &slots, std::uint16_t inputFrameId, std::uint16_t outputFrameId, std::uint16_t sendClockFactor=32, std::uint16_t reductionRatio=32, int watchdogFactor=3, std::uint16_t cSduLength=40)
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.
MacAddress GetInterfaceMac(const std::string &interface)
Get the MAC address assigned to a network interface.
AlarmNotification ParseAlarmNotification(const Bytes &data)
Parse a complete AlarmNotification PDU (BlockHeader + body + items).
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 int macAddressLength
Constant lenght of a mac address.
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.
Alarm endpoint configuration.
int transport
Transport: 0 = Layer2 (RTA), 1 = UDP.
std::string interface
Network interface name to bind to, e.g. "eth0".
std::uint16_t controllerRef
Controller's local alarm reference (from AlarmCRBlockReq).
std::uint16_t deviceRef
Device's local alarm reference (from AlarmCRBlockRes).
MacAddress deviceMac
The device's MAC address.
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 ModuleDiffBlock (0x8104).
Parsed PDRealData (0xF841) structure.
Complete device information summary.
std::uint16_t deviceId
PROFINET device ID (from DCP).
std::string name
Station name (from DCP).
std::string deviceType
Device type string (from DCP).
std::string netmask
IPv4 subnet mask (from DCP).
std::string annotation
Device model, if the EPM endpoint advertised one.
std::string mac
MAC address, formatted as a string (from DCP).
std::string SerialNumber() const
Serial number from I&M0, trimmed.
std::vector< std::string > deviceRoles
Decoded device role names (from DCP).
std::string vendorName
Human-readable vendor name (from DCP).
std::optional< PNInM0 > im0
I&M0 identification data, if it was read successfully.
std::string gateway
IPv4 default gateway (from DCP).
std::uint16_t vendorId
PROFINET vendor ID (from DCP).
std::uint16_t HardwareRevision() const
Hardware revision from I&M0.
std::string OrderId() const
Order ID from I&M0, trimmed.
std::string ip
IPv4 address (from DCP).
std::string SoftwareRevision() const
Software revision string from I&M0.
std::optional< blocks::PDRealData > topology
Physical topology, if it was read successfully.
Complete diagnosis data for one slot/subslot.
All I&M records a device supports, read in one call.
Configuration for establishing cyclic IO alongside an AR.
std::uint16_t watchdogFactor
Watchdog factor (missed-frame tolerance before a timeout fault).
std::vector< IOSlot > slots
Slots to include in the cyclic data frames.
std::uint16_t reductionRatio
Reduction ratio relative to the send clock.
std::uint16_t sendClockFactor
Send clock base factor (31.25us units).
Configuration options for an RPC connection.
double timeoutSec
Timeout in seconds for RPC operations.
std::unique_ptr< cyclic::CyclicController > controller
std::unique_ptr< asio::io_context > ioContext
Shared ASIO context.
Generic helpers for reading and writing integer and byte data in big- and little-endian wire formats.