23std::string Trim(
const std::string& s)
25 const std::size_t start = s.find_first_not_of(
" \t\r\n\0", 0, 4);
26 if (start == std::string::npos)
30 const std::size_t end = s.find_last_not_of(
" \t\r\n\0", std::string::npos, 4);
31 return s.substr(start, end - start + 1);
35Bytes 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))
237 auto targetMac =
ParseMac(identifier);
242 for (
const auto& [
Mac, Blocks] : responses)
244 if (
Mac == *targetMac)
258 std::cout << device.ToString() <<
"\n\n";
269 for (
const auto& [
Mac, Blocks] : responses)
299 rpc = std::make_unique<rpc::RPCCon>(
info, options);
346 catch (
const std::exception& e)
355 if (this->
rpc.get() == &releaseRpc)
389 returnInfo.
im0 = conn.ReadIm0();
399 for (
const auto& ep : endpoints)
401 if (!ep.annotation.empty())
408 catch (
const std::exception&)
416 returnInfo.
topology = conn.ReadPdRealData();
432 auto responseIOD = conn.Read(api, slot, subslot, index);
433 return responseIOD.payload;
448 return rpc->ReadImplicit(api, slot, subslot, index).payload;
473 conn.Write(api, slot, subslot, index, data);
479 std::vector<rpc::WriteItem> items;
480 items.reserve(writes.size());
481 for (
const auto& w : writes)
483 items.push_back({.slot = w.slot, .subslot = w.subslot, .index = w.index, .data = w.data, .api = w.api});
485 return conn.WriteMultiple(items);
517void ProfinetDevice::WriteIm1(
const std::string& tagFunction,
const std::string& tagLocation, std::uint16_t slot, std::uint16_t subslot)
519 if (tagFunction.size() > 32)
521 throw std::invalid_argument(
"tag_function exceeds 32 character limit");
523 if (tagLocation.size() > 22)
525 throw std::invalid_argument(
"tag_location exceeds 22 character limit");
528 Bytes data = BuildImBlockHeader(0x0021, 58);
529 data.push_back(0x00);
530 data.push_back(0x00);
531 Bytes func = Latin1Pad(tagFunction, 32);
532 Bytes loc = Latin1Pad(tagLocation, 22);
533 data.insert(data.end(), func.begin(), func.end());
534 data.insert(data.end(), loc.begin(), loc.end());
541 if (date.size() > 16)
543 throw std::invalid_argument(
"date exceeds 16 character limit");
546 Bytes data = BuildImBlockHeader(0x0022, 20);
547 data.push_back(0x00);
548 data.push_back(0x00);
549 Bytes dateBytes = Latin1Pad(date, 16);
550 data.insert(data.end(), dateBytes.begin(), dateBytes.end());
557 if (descriptor.size() > 54)
559 throw std::invalid_argument(
"descriptor exceeds 54 character limit");
562 Bytes data = BuildImBlockHeader(0x0023, 58);
563 data.push_back(0x00);
564 data.push_back(0x00);
565 Bytes descBytes = Latin1Pad(descriptor, 54);
566 data.insert(data.end(), descBytes.begin(), descBytes.end());
602 const Bytes data =
Read(slot, subslot, index);
603 if (data.size() >= 26)
629 throw std::runtime_error(
"Must be connected first");
632 if (!
rpc->HasAlarmCr())
634 throw std::runtime_error(
"AlarmCR not established. Reconnect with with_alarm_cr=true via RPCCon directly");
640 endpoint.
deviceRef =
static_cast<std::uint16_t
>(
rpc->DeviceAlarmRef());
671 int maxConsecutiveTimeouts,
672 std::unique_ptr<cyclic::ICyclicController> controller)
676 auto result = conn.Connect(
srcMac,
true, iocrSetup);
677 if (!result || !result->HasCyclic())
679 throw std::runtime_error(
"Cyclic IO not established by device");
683 conn.ApplicationReady(30.0);
688 if (controller ==
nullptr)
690 controller = std::make_unique<cyclic::CyclicController>(
698 std::uint16_t subslot)
713std::vector<ProfinetDevice>
Scan(
const std::string& interface,
double timeoutSec)
721 std::vector<ProfinetDevice> devices;
722 devices.reserve(responses.size());
723 for (
const auto& [Mac, Blocks] : responses)
726 devices.emplace_back(std::move(info), interface, srcMac, 5.0);
731std::map<std::string, DeviceInfo>
ScanDict(
const std::string& interface,
double timeoutSec)
733 std::map<std::string, DeviceInfo> result;
734 for (
auto& dev :
Scan(interface, timeoutSec))
737 info.
name = dev.Name();
739 info.
mac = dev.Mac();
740 result[dev.Name()] = info;
Device not found via DCP.
A minimal RAII wrapper around a Linux AF_PACKET raw socket bound to an interface.
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).
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.
~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.
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.
void Close()
Stop the alarm listener (if running) and fully close the RPC connection.
bool connected
Whether the AR is currently established.
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(const EthernetSocket &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.
ResponseMap ReadResponse(const EthernetSocket &sock, const MacAddress &myMac, int 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::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)
Scan the network and return a name -> DeviceInfo map.
std::vector< ProfinetDevice > Scan(const std::string &interface="eth0", double timeoutSec=3.0)
Scan the network for PROFINET devices via DCP Identify.
dcp::DCPDeviceDescription GetStationInfo(const EthernetSocket &sock, const MacAddress &src, const std::string &name, int timeoutSec=static_cast< int >(DEFAULT_TIMEOUT *2))
Resolve a device by PROFINET station name via DCP.
std::vector< EPMEndpoint > EpmLookup(asio::io_context &ioContext, const std::string &ip, std::uint16_t port=RPC_PORT, double timeoutSec=DEFAULT_TIMEOUT, std::optional< std::string > interfaceFilter=std::nullopt)
Query a device's Endpoint Mapper for available RPC endpoints.
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=43)
void PutU16(std::vector< std::uint8_t > &out, std::uint16_t v)
Append a 16-bit value to a buffer in big-endian order.
void PutU8(std::vector< std::uint8_t > &out, std::uint8_t v)
Append a single byte to a buffer.
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.
MacAddress GetMac(const std::string &ifname)
Get the MAC address of a network interface.
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.
Small helpers for reading/writing big-endian ("network order") integers to/from byte buffers....