PROFINET IO Controller Stack 1.0.0
Modern C++ implementation of a PROFINET IO Controller stack
Loading...
Searching...
No Matches
alarmListener.cpp
Go to the documentation of this file.
1
3
5
6#include <arpa/inet.h>
7#include <sys/socket.h>
8#include <unistd.h>
9
10#include <cstring>
11
12#include "profinet/exceptions.h"
13
14namespace profinet
15{
16
18 std::optional<MacAddress> controllerMac,
19 std::unique_ptr<IRawEthernetSocket> l2Socket)
20 : endpoint(std::move(endpoint)),
21 controllerMac(controllerMac.value_or(MacAddress{})),
22 l2Sock(std::move(l2Socket))
23{
24}
25
30
31void AlarmListener::AddCallback(std::function<void(const AlarmNotification&)> callback)
32{
33 if (!callback)
34 {
35 return;
36 }
37 const std::lock_guard<std::mutex> lock(callbacksMutex);
38 callbacks.push_back(std::move(callback));
39}
40
41void AlarmListener::RemoveCallback(std::function<void(const AlarmNotification&)> callback)
42{
43 if (!callback)
44 {
45 return;
46 }
47 // Extract the raw function pointer from the passed callback
48 auto targetPtr = callback.target<void (*)(const AlarmNotification&)>();
49 if (!targetPtr || !*targetPtr)
50 {
51 return; // Passed callback is not a plain function pointer (e.g., a capturing lambda)
52 }
53
54 auto fnToMatch = *targetPtr;
55
56 // Erase matching callbacks
57 const std::lock_guard<std::mutex> lock(callbacksMutex);
58 std::erase_if(callbacks, [fnToMatch](const auto& cb)
59 {
60 auto target = cb.template target<void (*)(const AlarmNotification&)>();
61 return target && *target == fnToMatch;
62 });
63}
64
66{
67 if (running)
68 {
69 return;
70 }
71
72 running = true;
73
74 if (endpoint.transport == 0)
75 {
76 if (l2Sock == nullptr)
77 {
78 l2Sock = std::make_unique<EthernetSocket>(endpoint.interface, ETHERTYPE_PROFINET);
79 }
80 static constexpr uint16_t timeoutMs = 1000;
81 l2Sock->SetTimeout(std::chrono::milliseconds(timeoutMs));
82 }
83 else
84 {
85 udpFd = ::socket(AF_INET, SOCK_DGRAM, 0);
86 if (udpFd < 0)
87 {
88 throw SocketError("Failed to create UDP alarm socket");
89 }
90 int reuse = 1;
91 ::setsockopt(udpFd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));
92
93 sockaddr_in addr{};
94 addr.sin_family = AF_INET;
95 addr.sin_port = htons(ALARM_UDP_PORT);
96 addr.sin_addr.s_addr = INADDR_ANY;
97 if (::bind(udpFd, reinterpret_cast<sockaddr*>(&addr), sizeof(addr)) < 0)
98 {
99 const int err = errno;
100 ::close(udpFd);
101 udpFd = -1;
102 running = false;
103 if (err == EACCES || err == EPERM)
104 {
105 throw PermissionDeniedError("Raw socket requires root/admin privileges");
106 }
107 throw SocketError(std::string("Failed to bind UDP alarm socket: ") + std::strerror(err));
108 }
109 timeval tv{};
110 tv.tv_sec = 1;
111 ::setsockopt(udpFd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
112 }
113
114 thread = std::thread([this]
115 { ListenLoop(); });
116}
117
119{
120 if (!running)
121 {
122 return;
123 }
124
125 running = false;
126
127 if (thread.joinable())
128 {
129 thread.join();
130 }
131
132 l2Sock.reset();
133
134 if (udpFd >= 0)
135 {
136 ::close(udpFd);
137 udpFd = -1;
138 }
139}
140
142{
143 while (running)
144 {
145 try
146 {
147 if (endpoint.transport == 0)
148 {
150 }
151 else
152 {
154 }
155 }
156 catch (const std::exception&)
157 {
158 if (!running)
159 {
160 break;
161 }
162 // Matches alarm_listener.py: log and keep the loop alive for
163 // non-fatal errors (parse failures, transient socket hiccups).
164 continue;
165 }
166 }
167}
168
170{
171 if (l2Sock == nullptr)
172 {
173 return;
174 }
175 Bytes data = l2Sock->Recv();
176 constexpr std::size_t macHeaderLength = 12U;
177 constexpr std::size_t etherTypeLength = 2U;
178 constexpr std::size_t frameIdLength = 2U;
179
180 if (data.size() < macHeaderLength + etherTypeLength + frameIdLength)
181 {
182 return;
183 }
184
185 MacAddress dstMac{};
186 MacAddress srcMac{};
187 std::size_t offset = 0;
188 std::ranges::copy(data.begin() + offset, data.begin() + offset + macAddressLength, dstMac.begin());
189 offset += macAddressLength;
190 std::ranges::copy(data.begin() + offset, data.begin() + offset + macAddressLength, srcMac.begin());
191 offset += macAddressLength;
192 if (srcMac != endpoint.deviceMac)
193 {
194 return;
195 }
196
197 // Returns the offset at which the EtherType starts.
198 const std::size_t etherTypeOffset = SkipVlanTags(data);
199
200 if (data.size() < etherTypeOffset + etherTypeLength + frameIdLength)
201 {
202 return;
203 }
204
205 auto ethertype = static_cast<std::uint16_t>((data[etherTypeOffset] << OneOctetShift) | data[etherTypeOffset + 1]);
206
207 if (ethertype != ETHERTYPE_PROFINET)
208 {
209 return;
210 }
211 const std::size_t frameIdOffset = etherTypeOffset + etherTypeLength;
212 auto frameId = static_cast<std::uint16_t>((data[frameIdOffset] << OneOctetShift) | data[frameIdOffset + 1]);
213 // offset += 2;
214 const std::size_t rtaOffset = frameIdOffset + frameIdLength;
215 // offset = 1; //(so we are 1 based not 0 based, so 16!)
216
217 if (frameId == FRAME_ID_ALARM_HIGH)
218 {
219 ProcessAlarm(Bytes(data.begin() + rtaOffset, data.end()), true, srcMac);
220 }
221 else if (frameId == FRAME_ID_ALARM_LOW)
222 {
223 ProcessAlarm(Bytes(data.begin() + rtaOffset, data.end()), false, srcMac);
224 }
225}
226
228{
229 if (udpFd < 0)
230 {
231 return;
232 }
234 sockaddr_in from{};
235 socklen_t fromLen = sizeof(from);
236 const ssize_t n = ::recvfrom(udpFd, buf.data(), buf.size(), 0, reinterpret_cast<sockaddr*>(&from), &fromLen);
237 if (n < 0)
238 {
239 if (errno == EAGAIN || errno == EWOULDBLOCK)
240 {
241 return;
242 }
243 if (running)
244 {
245 throw SocketError(std::string("Alarm listener socket error: ") + std::strerror(errno));
246 }
247 return;
248 }
249 buf.resize(static_cast<std::size_t>(n));
250 if (buf.size() < 28)
251 {
252 return;
253 }
254
255 lastUdpSrc = from;
256 ProcessAlarm(buf, std::nullopt, std::nullopt);
257}
258
259void AlarmListener::ProcessAlarm(const Bytes& payload, std::optional<bool> highPriority,
260 std::optional<MacAddress> srcMac)
261{
262 Bytes alarmData;
263
264 if (endpoint.transport == 0 && payload.size() >= 12)
265 {
266 PNRTAHeader rta;
267 try
268 {
269 rta = PNRTAHeader::Parse(Bytes(payload.begin(), payload.begin() + 12));
270 }
271 catch (const std::exception&)
272 {
273 return;
274 }
276 {
277 return;
278 }
280 alarmData = Bytes(payload.begin() + 12, payload.end());
281
282 auto version = (rta.pduType >> 4) & 0xFF;
284 {
285 return;
286 }
287
288 if ((rta.addFlags & PNRTAHeader::ADD_FLAGS_TACK) == 0)
289 {
290 return;
291 }
292 // expected seqnr & SendNack maken
293 // ook test:DuplicateNotificationIsNotDispatchedTwice
294 // if rta_header.send_seq_num == self._exp_seq_num_o:
295 // # Retransmission of an already-accepted PDU: re-ack only
296 // self._send_transport_ack(src_mac, bool(high_priority))
297 // return
298 }
299 else
300 {
301 alarmData = payload;
302 }
303
304 AlarmNotification alarm;
305 try
306 {
307 alarm = ParseAlarmNotification(alarmData);
308 }
309 catch (const std::exception&)
310 {
311 return;
312 }
313 (void)highPriority; // frame-ID priority is informational only; block_type is authoritative (matches upstream).
314
315 SendAck(alarm, srcMac);
316
317 std::vector<std::function<void(const AlarmNotification&)>> callbacksCopy;
318
319 {
320 std::scoped_lock lock(callbacksMutex);
321 callbacksCopy = callbacks;
322 }
323
324 for (const auto& cb : callbacksCopy)
325 {
326 try
327 {
328 cb(alarm);
329 }
330 catch (...)
331 {
332 // A misbehaving callback must not take down the listener thread.
333 }
334 }
335}
336
337void AlarmListener::SendAck(const AlarmNotification& alarm, std::optional<MacAddress> srcMac)
338{
339 try
340 {
342
343 PNBlockHeader header;
344 header.blockType = blockType;
345 header.blockLength = static_cast<std::uint16_t>(PNAlarmAckPDU::kSize - 4);
346 header.blockVersionHigh = 0x01;
347 header.blockVersionLow = 0x00;
348
349 auto alarmSpecifier =
350 static_cast<std::uint16_t>((alarm.alarmSequenceNumber & 0x07FF) |
351 (alarm.channelDiagnosis ? 0x0800 : 0) |
352 (alarm.manufacturerSpecific ? 0x1000 : 0) |
353 (alarm.submoduleDiagnosisState ? 0x2000 : 0) |
354 (alarm.arDiagnosisState ? 0x4000 : 0));
355
356 PNAlarmAckPDU ack;
357 std::memcpy(ack.blockHeader.data(), header.ToBytes().data(), blockHeaderLenght);
358 ack.alarmType = alarm.alarmType;
359 ack.api = alarm.api;
360 ack.slotNumber = alarm.slotNumber;
361 ack.subslotNumber = alarm.subslotNumber;
362 ack.alarmSpecifier = alarmSpecifier;
363 ack.statusPNIO = 0;
364
365 const Bytes ackData = ack.ToBytes();
366
367 if (endpoint.transport == 0)
368 {
369 SendLayer2Ack(ackData, srcMac.value_or(endpoint.deviceMac), alarm.IsHighPriority());
370 }
371 else if (lastUdpSrc)
372 {
373 SendUdpAck(ackData, *lastUdpSrc);
374 }
375 }
376 catch (const std::exception&)
377 {
378 // Best-effort ack, matches alarm_listener.py's catch-and-log.
379 }
380}
381namespace
382{
383constexpr std::uint16_t kVlanTpid = 0x8100;
384constexpr std::uint16_t kVlanPcp6Vid0 = 0xC000;
385constexpr std::uint16_t kVlanPcp5Vid0 = 0xA000;
386} // namespace
387void AlarmListener::SendLayer2Ack(const Bytes& ackData, const MacAddress& dstMac, bool highPriority)
388{
389 if (l2Sock == nullptr)
390 {
391 return;
392 }
393
394 sendSeqNum = static_cast<std::uint16_t>((sendSeqNum + 1) & 0xFFFF);
395
396 PNRTAHeader rta;
399 // rta.pduType = static_cast<std::uint8_t>((PNRTAHeader::RTA_TYPE_DATA << 4) | PNRTAHeader::VERSION_1);
400 rta.pduType = static_cast<std::uint8_t>((PNRTAHeader::VERSION_1 << 4) | PNRTAHeader::RTA_TYPE_DATA);
403 rta.ackSeqNum = recvSeqNum;
404 rta.variablePartLenght = static_cast<std::uint16_t>(ackData.size());
405
406 Bytes frame;
407 frame.insert(frame.end(), dstMac.begin(), dstMac.end());
408 frame.insert(frame.end(), controllerMac.begin(), controllerMac.end());
409 // add VLAN tag?
410 const std::uint16_t vlanTci = highPriority ? kVlanPcp6Vid0 : kVlanPcp5Vid0;
411
412 frame.push_back(static_cast<std::uint8_t>(kVlanTpid >> OneOctetShift));
413 frame.push_back(static_cast<std::uint8_t>(kVlanTpid & LowByteMask));
414 frame.push_back(static_cast<std::uint8_t>(vlanTci >> OneOctetShift));
415 frame.push_back(static_cast<std::uint8_t>(vlanTci & LowByteMask));
416
417 frame.push_back(static_cast<std::uint8_t>(ETHERTYPE_PROFINET >> OneOctetShift));
418 frame.push_back(static_cast<std::uint8_t>(ETHERTYPE_PROFINET & LowByteMask));
419 const std::uint16_t frameId = highPriority ? FRAME_ID_ALARM_HIGH : FRAME_ID_ALARM_LOW;
420 frame.push_back(static_cast<std::uint8_t>(frameId >> OneOctetShift));
421 frame.push_back(static_cast<std::uint8_t>(frameId & LowByteMask));
422 Bytes rtaBytes = rta.ToBytes();
423 frame.insert(frame.end(), rtaBytes.begin(), rtaBytes.end());
424 frame.insert(frame.end(), ackData.begin(), ackData.end());
425
426 try
427 {
428 l2Sock->Send(frame);
429 }
430 catch (const std::exception&)
431 {
432 // matches alarm_listener.py's catch-and-log
433 }
434}
435
436void AlarmListener::SendUdpAck(const Bytes& ackData, const sockaddr_in& dstAddr) const
437{
438 if (udpFd < 0)
439 {
440 return;
441 }
442 ::sendto(udpFd, ackData.data(), ackData.size(), 0, reinterpret_cast<const sockaddr*>(&dstAddr),
443 sizeof(dstAddr));
444}
445
446} // namespace profinet
Declares the PROFINET RTA alarm listener and AlarmAck transmission support.
std::atomic< bool > running
Whether the listener thread should keep running.
void SendAck(const AlarmNotification &alarm, std::optional< MacAddress > srcMac)
Build and send an AlarmAck-PDU for a received notification.
AlarmEndpoint endpoint
Alarm endpoint configuration.
void HandleUdpFrame()
Receive and process one UDP frame.
~AlarmListener() override
Stop (if running) and release resources.
std::optional< sockaddr_in > lastUdpSrc
Address of the most recently received UDP alarm, for replying.
void Stop() override
Stop the listener.
std::uint16_t recvSeqNum
Last received sequence number, for duplicate detection.
std::unique_ptr< IRawEthernetSocket > l2Sock
Raw Ethernet transport used for Layer 2 alarm reception.
MacAddress controllerMac
This host's MAC address.
std::uint16_t sendSeqNum
Sequence number for outgoing AlarmAck-PDUs.
void HandleLayer2Frame()
Receive and process one Layer 2 (RTA-PDU) frame.
void SendUdpAck(const Bytes &ackData, const sockaddr_in &dstAddr) const
Send an AlarmAck-PDU over the UDP transport.
void ListenLoop()
Listener thread body: dispatches to the Layer 2 or UDP handler.
std::thread thread
Background thread receiving and processing alarms.
void RemoveCallback(std::function< void(const AlarmNotification &)> callback) override
Remove a registered callback.
void SendLayer2Ack(const Bytes &ackData, const MacAddress &dstMac, bool highPriority)
Send an AlarmAck-PDU over the Layer 2 transport.
std::vector< std::function< void(const AlarmNotification &)> > callbacks
Registered callbacks invoked for successfully parsed alarms.
void ProcessAlarm(const Bytes &payload, std::optional< bool > highPriority, std::optional< MacAddress > srcMac)
Parse an alarm payload and dispatch it to registered callbacks.
int udpFd
Socket file descriptor used for the UDP transport.
void AddCallback(std::function< void(const AlarmNotification &)> callback) override
Register a callback for received alarms.
void Start() override
Start the background listener (socket + thread).
AlarmListener(AlarmEndpoint endpoint, std::optional< MacAddress > controllerMac=std::nullopt, std::unique_ptr< IRawEthernetSocket > l2Socket=nullptr)
Construct a listener for the given endpoint.
std::mutex callbacksMutex
Protects concurrent access to callbacks.
Insufficient permissions for raw socket.
Definition exceptions.h:675
Socket operation error.
Definition exceptions.h:663
Declares exceptions and error types used by the PROFINET IO controller stack.
BlockType
PROFINET block types.
Definition indices.h:91
@ AlarmAcknowledgementLow
Alarm acknowledgement low block.
@ AlarmAcknowledgementHigh
Alarm acknowledgement high block.
AlarmNotification ParseAlarmNotification(const Bytes &data)
Parse a complete AlarmNotification PDU (BlockHeader + body + items).
Definition alarms.cpp:286
std::array< std::uint8_t, macAddressLength > MacAddress
A 6-byte Ethernet MAC address.
Definition util.h:67
constexpr std::uint16_t ALARM_UDP_PORT
UDP port used for alarm transport when Transport == 1.
std::size_t SkipVlanTags(std::span< const std::uint8_t > frame) noexcept
Return the byte offset of the real EtherType in a raw Ethernet frame.
Definition util.cpp:89
static constexpr int OneOctetShift
The bit-shift distance required to move data across a single octet.
Definition util.h:50
constexpr int macAddressLength
Constant lenght of a mac address.
Definition util.h:41
static constexpr std::uint8_t LowByteMask
Bitmask used to isolate the lowest significant byte (8 bits) of a larger integer.
Definition util.h:59
std::vector< std::uint8_t > Bytes
Generic byte buffer alias used throughout the library for raw wire data.
Definition protocol.h:52
constexpr int blockHeaderLenght
Constant lenght of a blockHeader.
Definition protocol.h:55
constexpr std::size_t RECEIVE_BUFFER_LENGTH
Receive buffer length.
Definition util.h:62
constexpr std::uint16_t FRAME_ID_ALARM_HIGH
Frame ID used for high-priority alarms.
constexpr std::uint16_t ETHERTYPE_PROFINET
EtherType used by PROFINET RTA frames (0x8892).
constexpr std::uint16_t FRAME_ID_ALARM_LOW
Frame ID used for low-priority alarms.
std::uint8_t version
Definition rpc.cpp:3247
Bytes payload
Definition rpc.cpp:3254
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.
Definition alarms.h:358
bool manufacturerSpecific
Whether the alarm specifier indicates a manufacturer-specific diagnosis.
Definition alarms.h:390
std::uint16_t slotNumber
Slot number where the alarm originated.
Definition alarms.h:372
std::uint16_t alarmSequenceNumber
Sequence number extracted from the alarm specifier.
Definition alarms.h:384
std::uint32_t api
API number.
Definition alarms.h:369
AlarmType alarmType
Alarm type (Diagnosis/Process/Pull/Plug/...).
Definition alarms.h:366
std::uint16_t subslotNumber
Subslot number where the alarm originated.
Definition alarms.h:375
bool IsHighPriority() const
Whether this is a high-priority notification.
Definition alarms.h:406
bool channelDiagnosis
Whether the alarm specifier indicates channel diagnosis is present.
Definition alarms.h:387
bool arDiagnosisState
Whether the alarm specifier indicates AR diagnosis state.
Definition alarms.h:396
bool submoduleDiagnosisState
Whether the alarm specifier indicates submodule diagnosis state.
Definition alarms.h:393
AlarmAck-PDU: acknowledges receipt of an AlarmNotification-PDU.
Definition protocol.h:1861
std::uint16_t alarmSpecifier
Alarm specifier flags echoed from the notification.
Definition protocol.h:1878
AlarmType alarmType
Alarm type being acknowledged.
Definition protocol.h:1866
std::uint16_t slotNumber
Slot number being acknowledged.
Definition protocol.h:1872
std::uint32_t statusPNIO
PNIO status code (0 = acknowledged OK).
Definition protocol.h:1881
std::uint16_t subslotNumber
Subslot number being acknowledged.
Definition protocol.h:1875
static constexpr std::size_t kSize
Size in bytes of this PDU.
Definition protocol.h:1884
std::array< std::uint8_t, blockHeaderLenght > blockHeader
Nested 6-byte block header.
Definition protocol.h:1863
std::uint32_t api
API number.
Definition protocol.h:1869
Bytes ToBytes() const
Serialize this PDU back to raw bytes.
Definition protocol.h:1888
Bytes ToBytes() const
Serialize this header back to raw bytes.
Definition protocol.h:906
BlockType blockType
Block type identifier.
Definition protocol.h:872
std::uint8_t blockVersionHigh
Block format major version.
Definition protocol.h:878
std::uint8_t blockVersionLow
Block format minor version.
Definition protocol.h:881
std::uint16_t blockLength
Length in bytes of the block body (excludes Type/Length fields).
Definition protocol.h:875
RTA-PDU Header - Real-Time Acyclic PDU for Layer 2 alarm transport.
Definition protocol.h:1904
static constexpr std::uint8_t ADD_FLAGS_TACK
TACK(transport ack request) in bit 4(IEC 61158-6-10; cf. p-net pf_put_alarm_fixed)
Definition protocol.h:1928
std::uint16_t sendSeqNum
Sender's sequence number for this PDU.
Definition protocol.h:1945
std::uint16_t alarmSrcEndpoint
Source alarm endpoint (AlarmCR reference on the sender).
Definition protocol.h:1936
Bytes ToBytes() const
Serialize this header and variable part back to raw bytes.
Definition protocol.h:1983
std::uint16_t alarmDstEndpoint
Destination alarm endpoint (AlarmCR reference on the receiver).
Definition protocol.h:1933
std::uint16_t variablePartLenght
Length in bytes of the variable part (Payload).
Definition protocol.h:1951
std::uint8_t addFlags
Additional flags (window size, TACK flag).
Definition protocol.h:1942
static PNRTAHeader Parse(const Bytes &data)
Parse an RTA header and variable part from raw bytes.
Definition protocol.h:1962
static constexpr std::uint8_t RTA_TYPE_DATA
PDU type: DATA.
Definition protocol.h:1907
static constexpr std::uint8_t ADD_FLAGS_WINDOW_1
RTA AddFlags bits: window size in bits 0-3.
Definition protocol.h:1925
static constexpr std::uint8_t VERSION_1
RTA protocol version 1.
Definition protocol.h:1919
std::uint16_t ackSeqNum
Sequence number being acknowledged.
Definition protocol.h:1948
std::uint8_t pduType
PDU type and protocol version, packed as type(bits 4-7) + version(bits 0-3).
Definition protocol.h:1939