PROFINET IO Controller Stack 1.0.0
Modern C++ implementation of a PROFINET IO Controller stack
Loading...
Searching...
No Matches
virtual_phoenix_plc.cpp
Go to the documentation of this file.
1
3
5
6#include <algorithm>
7#include <bit>
8#include <cfloat>
9#include <cmath>
10#include <cstring>
11#include <limits>
12#include <sstream>
13#include <stdexcept>
14#include <utility>
15
17
18namespace profinet::sim
19{
20
21namespace
22{
23
29const io::IoSignal* FindSignal(const io::IoSignalCatalog& catalog,
30 const std::string_view textId,
31 const io::IoSignalDirection direction)
32{
33 const auto& signals = direction == io::IoSignalDirection::Sensor
34 ? catalog.Sensors()
35 : catalog.Actuators();
36 const io::IoSignal* result = nullptr;
37 for (const auto& signal : signals)
38 {
39 if (signal.textId != textId)
40 {
41 continue;
42 }
43 if (result != nullptr)
44 {
45 throw std::invalid_argument("Signal is not unique: " + std::string(textId));
46 }
47 result = &signal;
48 }
49 return result;
50}
51
55void ValidateRange(const io::IoSignal& signal, const Bytes& data)
56{
57 if (signal.byteOffset > data.size() || signal.byteLength > data.size() - signal.byteOffset)
58 {
59 throw std::out_of_range("Signal lies outside the process-data object: " + signal.textId);
60 }
61}
62
67std::uint64_t ReadUnsigned(const Bytes& data, const io::IoSignal& signal)
68{
69 ValidateRange(signal, data);
70 if (signal.bitSignal)
71 {
72 if (signal.bitOffset >= 8U)
73 {
74 throw std::out_of_range("Signal bit offset exceeds one byte: " + signal.textId);
75 }
76 return (data[signal.byteOffset] & static_cast<std::uint8_t>(1U << signal.bitOffset)) != 0U ? 1U : 0U;
77 }
78
79 if (signal.byteLength > sizeof(std::uint64_t))
80 {
81 throw std::invalid_argument("Signal is wider than uint64_t: " + signal.textId);
82 }
83 std::uint64_t value = 0;
84 for (std::size_t index = 0; index < signal.byteLength; ++index)
85 {
86 value = (value << 8U) | data[signal.byteOffset + index];
87 }
88 return value;
89}
90
95void WriteUnsigned(Bytes& data, const io::IoSignal& signal, const std::uint64_t value)
96{
97 ValidateRange(signal, data);
98 if (signal.bitSignal)
99 {
100 if (signal.bitOffset >= 8U || value > 1U)
101 {
102 throw std::invalid_argument("Invalid Boolean value for signal: " + signal.textId);
103 }
104 const auto mask = static_cast<std::uint8_t>(1U << signal.bitOffset);
105 if (value != 0U)
106 {
107 data[signal.byteOffset] |= mask;
108 }
109 else
110 {
111 data[signal.byteOffset] &= static_cast<std::uint8_t>(~mask);
112 }
113 return;
114 }
115
116 if (signal.byteLength > sizeof(std::uint64_t))
117 {
118 throw std::invalid_argument("Signal is wider than uint64_t: " + signal.textId);
119 }
120 for (std::size_t index = 0; index < signal.byteLength; ++index)
121 {
122 const std::size_t shift = (signal.byteLength - index - 1U) * 8U;
123 data[signal.byteOffset + index] =
124 static_cast<std::uint8_t>((value >> shift) & 0xFFU);
125 }
126}
127
128} // namespace
129
131 const gsdml::GSDMLDevice& gsdmlRef,
132 MacAddress controllerMacRef)
133 : name(std::move(nameRef)),
134 gsdml(gsdmlRef),
135 controllerMac(controllerMacRef),
136 catalog(io::IoSignalCatalog::FromGsdml(this->gsdml))
137{
138 const std::size_t imageSize = std::max<std::size_t>(1U, RequiredImageSize(catalog));
139 ioImage.inputs.resize(imageSize, 0U);
140 ioImage.outputs.resize(imageSize, 0U);
141}
142
147
149 VirtualAumaValve& valve,
150 rt::IOCRConfig inputIocr,
151 rt::IOCRConfig outputIocr,
152 const int maxConsecutiveTimeouts)
153{
154 const std::string connectionName = valve.Name();
155
156 std::lock_guard<std::mutex> lock(mutex);
157 if (FindConnection(connectionName) != nullptr)
158 {
159 throw std::invalid_argument("Phoenix connection already exists: " + connectionName);
160 }
161
162 if (inputIocr.dataLength == 0U || outputIocr.dataLength == 0U)
163 {
164 throw std::invalid_argument("Phoenix connection requires non-zero IOCR data lengths");
165 }
166
167 auto connection = std::make_unique<Connection>();
168 connection->name = connectionName;
169 connection->valve = &valve;
170
171 // Determine the local output image before moving the IOCRs into the controller.
172 // The production controller takes ownership by value, so no moved-from IOCR is inspected.
173 for (const auto& object : outputIocr.objects)
174 {
175 if (object.slot == 2U && object.subslot == 1U && object.dataLength > 0U)
176 {
177 connection->outputImage.assign(object.dataLength, 0U);
178 break;
179 }
180 }
181 if (connection->outputImage.empty())
182 {
183 throw std::invalid_argument("Phoenix connection has no slot 2/1 output object");
184 }
185
187 txSocket->SetReceiveEnabled(false);
189
190 connection->controller = std::make_unique<cyclic::CyclicController>(
191 ioContext,
192 "virtual",
194 valve.DeviceMac(),
195 std::move(inputIocr),
196 std::move(outputIocr),
197 maxConsecutiveTimeouts,
199
200 connection->controller->Start(std::move(txSocket), std::move(rxSocket));
201 connections.push_back(std::move(connection));
202}
203
204void VirtualPhoenixPlc::Disconnect(const std::string& valveName)
205{
206 std::unique_ptr<Connection> removed;
207 {
208 std::lock_guard<std::mutex> lock(mutex);
209 const auto it = std::find_if(connections.begin(), connections.end(),
210 [&valveName](const std::unique_ptr<Connection>& connection)
211 {
212 return connection != nullptr && connection->name == valveName;
213 });
214 if (it == connections.end())
215 {
216 return;
217 }
218 removed = std::move(*it);
219 connections.erase(it);
220 }
221
222 if (removed != nullptr && removed->controller != nullptr)
223 {
224 removed->controller->Stop();
225 }
226}
227
229{
230 std::vector<std::unique_ptr<Connection>> local;
231 {
232 std::lock_guard<std::mutex> lock(mutex);
233 local.swap(connections);
234 }
235 for (auto& connection : local)
236 {
237 if (connection != nullptr && connection->controller != nullptr)
238 {
239 connection->controller->Stop();
240 }
241 }
242}
243
244void VirtualPhoenixPlc::SetOutput(const std::string& valveName,
245 const std::string_view signalTextId,
246 const double value)
247{
248 std::lock_guard<std::mutex> lock(mutex);
249 Connection* connection = FindConnection(valveName);
250 if (connection == nullptr || connection->controller == nullptr || connection->valve == nullptr)
251 {
252 throw std::invalid_argument("Unknown Phoenix connection: " + valveName);
253 }
254
255 const io::IoSignal* signal =
256 FindDeviceSignal(*connection->valve, signalTextId, io::IoSignalDirection::Actuator);
257 if (signal == nullptr)
258 {
259 throw std::invalid_argument("Unknown AUMA actuator signal: " + std::string(signalTextId));
260 }
261
262 const Bytes updated = EncodeSignalValue(*signal, connection->outputImage, value);
263 connection->outputImage = updated;
264 connection->controller->SetOutputData(connection->valve->OutputSlot(),
265 connection->valve->OutputSubslot(),
266 updated);
267}
268
269std::optional<double> VirtualPhoenixPlc::ReadInput(const std::string& valveName,
270 const std::string_view signalTextId) const
271{
272 std::lock_guard<std::mutex> lock(mutex);
273 const Connection* connection = FindConnection(valveName);
274 if (connection == nullptr || connection->controller == nullptr || connection->valve == nullptr)
275 {
276 return std::nullopt;
277 }
278
279 const io::IoSignal* signal =
280 FindDeviceSignal(*connection->valve, signalTextId, io::IoSignalDirection::Sensor);
281 if (signal == nullptr)
282 {
283 return std::nullopt;
284 }
285
286 const auto [slot, subslot] = ResolveDeviceLocation(*signal,
287 connection->valve->InputSlot(),
288 connection->valve->InputSubslot());
289 const auto data = connection->controller->GetInputData(slot, subslot);
290 if (!data.has_value())
291 {
292 return std::nullopt;
293 }
294 return DecodeSignal(*signal, *data);
295}
296
297void VirtualPhoenixPlc::SetLocalInputBytes(const std::string_view signalTextId,
298 const Bytes& data)
299{
300 std::lock_guard<std::mutex> lock(mutex);
301 const io::IoSignal* signal = FindLocalInputSignal(signalTextId);
302 if (signal == nullptr)
303 {
304 throw std::invalid_argument("Unknown Phoenix input signal: " + std::string(signalTextId));
305 }
306 if (signal->byteLength != data.size())
307 {
308 throw std::invalid_argument("Phoenix input byte length mismatch for signal: " +
309 std::string(signalTextId));
310 }
311 ValidateRange(*signal, ioImage.inputs);
312 std::copy(data.begin(), data.end(), ioImage.inputs.begin() + static_cast<std::ptrdiff_t>(signal->byteOffset));
313}
314
315std::optional<Bytes> VirtualPhoenixPlc::GetLocalInputBytes(const std::string_view signalTextId) const
316{
317 std::lock_guard<std::mutex> lock(mutex);
318 const io::IoSignal* signal = FindLocalInputSignal(signalTextId);
319 if (signal == nullptr)
320 {
321 return std::nullopt;
322 }
323 ValidateRange(*signal, ioImage.inputs);
324 return Bytes(ioImage.inputs.begin() + static_cast<std::ptrdiff_t>(signal->byteOffset),
325 ioImage.inputs.begin() +
326 static_cast<std::ptrdiff_t>(signal->byteOffset + signal->byteLength));
327}
328
329void VirtualPhoenixPlc::SetLocalInputValue(const std::string_view signalTextId, const double value)
330{
331 std::lock_guard<std::mutex> lock(mutex);
332 const io::IoSignal* signal = FindLocalInputSignal(signalTextId);
333 if (signal == nullptr)
334 {
335 throw std::invalid_argument("Unknown Phoenix input signal: " + std::string(signalTextId));
336 }
337 ioImage.inputs = EncodeSignalValue(*signal, ioImage.inputs, value);
338}
339
340std::optional<double> VirtualPhoenixPlc::GetLocalInputValue(const std::string_view signalTextId) const
341{
342 std::lock_guard<std::mutex> lock(mutex);
343 const io::IoSignal* signal = FindLocalInputSignal(signalTextId);
344 if (signal == nullptr)
345 {
346 return std::nullopt;
347 }
348 return DecodeSignal(*signal, ioImage.inputs);
349}
350
351void VirtualPhoenixPlc::SetLocalOutputBytes(const std::string_view signalTextId, const Bytes& data)
352{
353 std::lock_guard<std::mutex> lock(mutex);
354 const io::IoSignal* signal = FindLocalOutputSignal(signalTextId);
355 if (signal == nullptr)
356 {
357 throw std::invalid_argument("Unknown Phoenix output signal: " + std::string(signalTextId));
358 }
359 if (signal->byteLength != data.size())
360 {
361 throw std::invalid_argument("Phoenix output byte length mismatch for signal: " +
362 std::string(signalTextId));
363 }
364 ValidateRange(*signal, ioImage.outputs);
365 std::copy(data.begin(), data.end(),
366 ioImage.outputs.begin() + static_cast<std::ptrdiff_t>(signal->byteOffset));
367}
368
369std::optional<Bytes> VirtualPhoenixPlc::GetLocalOutputBytes(const std::string_view signalTextId) const
370{
371 std::lock_guard<std::mutex> lock(mutex);
372 const io::IoSignal* signal = FindLocalOutputSignal(signalTextId);
373 if (signal == nullptr)
374 {
375 return std::nullopt;
376 }
377 ValidateRange(*signal, ioImage.outputs);
378 return Bytes(ioImage.outputs.begin() + static_cast<std::ptrdiff_t>(signal->byteOffset),
379 ioImage.outputs.begin() +
380 static_cast<std::ptrdiff_t>(signal->byteOffset + signal->byteLength));
381}
382
383void VirtualPhoenixPlc::SetLocalOutputValue(const std::string_view signalTextId, const double value)
384{
385 std::lock_guard<std::mutex> lock(mutex);
386 const io::IoSignal* signal = FindLocalOutputSignal(signalTextId);
387 if (signal == nullptr)
388 {
389 throw std::invalid_argument("Unknown Phoenix output signal: " + std::string(signalTextId));
390 }
392}
393
394std::optional<double> VirtualPhoenixPlc::GetLocalOutputValue(const std::string_view signalTextId) const
395{
396 std::lock_guard<std::mutex> lock(mutex);
397 const io::IoSignal* signal = FindLocalOutputSignal(signalTextId);
398 if (signal == nullptr)
399 {
400 return std::nullopt;
401 }
402 return DecodeSignal(*signal, ioImage.outputs);
403}
404
406{
407 return catalog;
408}
409
411{
412 return controllerMac;
413}
414
415const std::string& VirtualPhoenixPlc::Name() const noexcept
416{
417 return name;
418}
419
421{
422 std::lock_guard<std::mutex> lock(mutex);
423 return connections.size();
424}
425
426std::optional<cyclic::CyclicState>
427VirtualPhoenixPlc::ConnectionState(const std::string& valveName) const
428{
429 std::lock_guard<std::mutex> lock(mutex);
430 const Connection* connection = FindConnection(valveName);
431 if (connection == nullptr || connection->controller == nullptr)
432 {
433 return std::nullopt;
434 }
435 return connection->controller->State();
436}
437
438std::optional<cyclic::CyclicStatsSnapshot>
439VirtualPhoenixPlc::ConnectionStats(const std::string& valveName) const
440{
441 std::lock_guard<std::mutex> lock(mutex);
442 const Connection* connection = FindConnection(valveName);
443 if (connection == nullptr || connection->controller == nullptr)
444 {
445 return std::nullopt;
446 }
447 return connection->controller->StatsSnapshot();
448}
449
451{
452 std::lock_guard<std::mutex> lock(mutex);
453 std::ostringstream out;
454 out << "Phoenix PLC " << name << " MAC " << Mac2String(controllerMac) << '\n';
455 out << " connections: " << connections.size() << '\n';
456 for (const auto& connection : connections)
457 {
458 if (connection == nullptr || connection->controller == nullptr || connection->valve == nullptr)
459 {
460 continue;
461 }
462 const auto stats = connection->controller->StatsSnapshot();
463 out << " - " << connection->name
464 << " state=" << cyclic::ToString(connection->controller->State())
465 << " position=" << connection->valve->Position()
466 << " target=" << connection->valve->TargetPosition()
467 << " scenario=" << ToString(connection->valve->Scenario())
468 << " rx=" << stats.framesReceived
469 << " missed=" << stats.framesMissed
470 << '\n';
471 }
472 return out.str();
473}
474
476{
477 for (auto& connection : connections)
478 {
479 if (connection != nullptr && connection->name == valveName)
480 {
481 return connection.get();
482 }
483 }
484 return nullptr;
485}
486
488VirtualPhoenixPlc::FindConnection(const std::string& valveName) const
489{
490 for (const auto& connection : connections)
491 {
492 if (connection != nullptr && connection->name == valveName)
493 {
494 return connection.get();
495 }
496 }
497 return nullptr;
498}
499
501 const std::string_view textId,
502 const io::IoSignalDirection direction)
503{
504 return FindSignal(valve.SignalCatalog(), textId, direction);
505}
506
507double VirtualPhoenixPlc::DecodeSignal(const io::IoSignal& signal, const Bytes& data) const
508{
509 const std::uint64_t raw = ReadUnsigned(data, signal);
510 switch (signal.valueType)
511 {
513 return raw == 0U ? 0.0 : 1.0;
515 return static_cast<double>(raw);
517 {
518 const std::size_t bits = signal.byteLength * 8U;
519 if (bits == 0U || bits > 64U)
520 {
521 throw std::invalid_argument("Invalid signed integer width: " + signal.textId);
522 }
523 if (bits == 64U)
524 {
525 const std::int64_t signedValue = std::bit_cast<std::int64_t>(raw);
526 return static_cast<double>(signedValue);
527 }
528 const std::uint64_t signBit = 1ULL << (bits - 1U);
529 const std::uint64_t mask = ~0ULL << bits;
530 const std::int64_t signedValue = (raw & signBit) != 0U
531 ? static_cast<std::int64_t>(raw | mask)
532 : static_cast<std::int64_t>(raw);
533 return static_cast<double>(signedValue);
534 }
536 if (signal.byteLength == sizeof(float))
537 {
538 const std::uint32_t bits = static_cast<std::uint32_t>(raw);
539 float value = 0.0F;
540 std::memcpy(&value, &bits, sizeof(value));
541 return static_cast<double>(value);
542 }
543 if (signal.byteLength == sizeof(double))
544 {
545 double value = 0.0;
546 std::memcpy(&value, &raw, sizeof(value));
547 return value;
548 }
549 throw std::invalid_argument("Unsupported floating-point width: " + signal.textId);
551 throw std::invalid_argument("Unsupported Phoenix simulator signal: " + signal.textId);
552 }
553 throw std::logic_error("Unknown IoSignalValueType");
554}
555
557 const Bytes& data,
558 const double value) const
559{
560 Bytes result = data;
561 if (!signal.IsDoubleCompatible())
562 {
563 throw std::invalid_argument("Signal is not double-compatible: " + signal.textId);
564 }
565 if (!std::isfinite(value))
566 {
567 throw std::invalid_argument("Signal value must be finite: " + signal.textId);
568 }
569
571 {
572 if (value != 0.0 && value != 1.0)
573 {
574 throw std::invalid_argument("Boolean signal requires value 0 or 1: " + signal.textId);
575 }
576 WriteUnsigned(result, signal, value == 0.0 ? 0U : 1U);
577 return result;
578 }
579
581 {
582 ValidateRange(signal, result);
583 if (signal.byteLength == sizeof(float))
584 {
585 if (static_cast<long double>(value) > static_cast<long double>(FLT_MAX) ||
586 static_cast<long double>(value) < -static_cast<long double>(FLT_MAX))
587 {
588 throw std::out_of_range("Float32 signal value is outside its range: " + signal.textId);
589 }
590 const float floatValue = static_cast<float>(value);
591 const std::uint32_t bits = std::bit_cast<std::uint32_t>(floatValue);
592 for (std::size_t index = 0; index < sizeof(bits); ++index)
593 {
594 const std::size_t shift = (sizeof(bits) - index - 1U) * 8U;
595 result[signal.byteOffset + index] =
596 static_cast<std::uint8_t>((bits >> shift) & 0xFFU);
597 }
598 return result;
599 }
600 if (signal.byteLength == sizeof(double))
601 {
602 const std::uint64_t bits = std::bit_cast<std::uint64_t>(value);
603 for (std::size_t index = 0; index < sizeof(bits); ++index)
604 {
605 const std::size_t shift = (sizeof(bits) - index - 1U) * 8U;
606 result[signal.byteOffset + index] =
607 static_cast<std::uint8_t>((bits >> shift) & 0xFFU);
608 }
609 return result;
610 }
611 throw std::invalid_argument("Unsupported floating-point width: " + signal.textId);
612 }
613
614 const long double integerValue = static_cast<long double>(value);
615 if (std::floor(integerValue) != integerValue)
616 {
617 throw std::invalid_argument("Integer signal requires an integral value: " + signal.textId);
618 }
619 if (signal.byteLength == 0U || signal.byteLength > sizeof(std::uint64_t))
620 {
621 throw std::invalid_argument("Integer signal has an invalid width: " + signal.textId);
622 }
623
624 const std::size_t bits = signal.byteLength * 8U;
626 {
627 if (integerValue < 0.0L)
628 {
629 throw std::invalid_argument("Unsigned signal cannot be negative: " + signal.textId);
630 }
631 const long double maxValue = bits == 64U
632 ? 18446744073709551615.0L
633 : static_cast<long double>((1ULL << bits) - 1ULL);
634 if (integerValue > maxValue)
635 {
636 throw std::out_of_range("Unsigned signal value exceeds its range: " + signal.textId);
637 }
638 WriteUnsigned(result, signal, static_cast<std::uint64_t>(value));
639 return result;
640 }
641
643 {
644 const long double minValue = bits == 64U
645 ? -9223372036854775808.0L
646 : -static_cast<long double>(1ULL << (bits - 1U));
647 const long double maxValue = bits == 64U
648 ? 9223372036854775807.0L
649 : static_cast<long double>((1ULL << (bits - 1U)) - 1ULL);
650 if (integerValue < minValue || integerValue > maxValue)
651 {
652 throw std::out_of_range("Signed signal value exceeds its range: " + signal.textId);
653 }
654 const auto signedValue = static_cast<std::int64_t>(value);
655 WriteUnsigned(result, signal, std::bit_cast<std::uint64_t>(signedValue));
656 return result;
657 }
658
659 throw std::invalid_argument("Unsupported signal value type: " + signal.textId);
660}
661
663{
664 std::size_t size = 0;
665 const auto accumulate = [&size](const std::vector<io::IoSignal>& signals)
666 {
667 for (const auto& signal : signals)
668 {
669 size = std::max(size, signal.byteOffset + signal.byteLength);
670 }
671 };
672 accumulate(signalCatalog.Sensors());
673 accumulate(signalCatalog.Actuators());
674 return size;
675}
676
677const io::IoSignal* VirtualPhoenixPlc::FindLocalInputSignal(const std::string_view textId) const
678{
679 return FindSignal(catalog, textId, io::IoSignalDirection::Sensor);
680}
681
682const io::IoSignal* VirtualPhoenixPlc::FindLocalOutputSignal(const std::string_view textId) const
683{
684 return FindSignal(catalog, textId, io::IoSignalDirection::Actuator);
685}
686
687std::pair<std::uint16_t, std::uint16_t> VirtualPhoenixPlc::ResolveDeviceLocation(
688 const io::IoSignal& signal,
689 const std::uint16_t defaultSlot,
690 const std::uint16_t defaultSubslot)
691{
692 std::uint16_t slot = defaultSlot;
693 std::uint16_t subslot = defaultSubslot;
694 if (!signal.possibleSlots.empty())
695 {
696 if (signal.possibleSlots.size() != 1U || signal.possibleSlots.front() < 0 ||
697 signal.possibleSlots.front() > std::numeric_limits<std::uint16_t>::max())
698 {
699 throw std::invalid_argument("AUMA signal has ambiguous slot placement: " + signal.textId);
700 }
701 slot = static_cast<std::uint16_t>(signal.possibleSlots.front());
702 }
703 if (!signal.possibleSubslots.empty())
704 {
705 if (signal.possibleSubslots.size() != 1U || signal.possibleSubslots.front() < 0 ||
706 signal.possibleSubslots.front() > std::numeric_limits<std::uint16_t>::max())
707 {
708 throw std::invalid_argument("AUMA signal has ambiguous subslot placement: " + signal.textId);
709 }
710 subslot = static_cast<std::uint16_t>(signal.possibleSubslots.front());
711 }
712 return {slot, subslot};
713}
714
715} // namespace profinet::sim
Builds and owns the logical sensor and actuator signals described by a GSDML device.
Definition IOSignal.h:186
const std::vector< IoSignal > & Actuators() const noexcept
Returns all non-reserved actuator/output signals.
Definition IOSignal.cpp:539
const std::vector< IoSignal > & Sensors() const noexcept
Returns all non-reserved sensor/input signals.
Definition IOSignal.cpp:534
Small virtual Ethernet switch suitable for deterministic tests and demos.
std::unique_ptr< InMemoryEthernetSocket > CreateSocket(MacAddress localMac)
Create one virtual raw-Ethernet endpoint.
AUMA SGx application model that behaves as a real RTC1 peer in memory.
const MacAddress & DeviceMac() const noexcept
Return the simulated device MAC address.
std::uint16_t OutputSubslot() const noexcept
Return the configured output subslot.
const std::string & Name() const noexcept
Return the configured simulator name.
std::uint16_t InputSlot() const noexcept
Return the configured input slot.
const io::IoSignalCatalog & SignalCatalog() const noexcept
Return the GSDML-derived signal catalogue.
std::uint16_t OutputSlot() const noexcept
Return the configured output slot.
std::uint16_t InputSubslot() const noexcept
Return the configured input subslot.
void SetLocalOutputValue(std::string_view signalTextId, double value)
Set one typed Phoenix output signal.
std::optional< double > ReadInput(const std::string &valveName, std::string_view signalTextId) const
Read a typed AUMA input signal using the production controller API.
void SetLocalOutputBytes(std::string_view signalTextId, const Bytes &data)
Set raw bytes in the local Phoenix output process image.
const std::string & Name() const noexcept
Return the simulator instance name.
void SetOutput(const std::string &valveName, std::string_view signalTextId, double value)
Set a typed AUMA actuator signal.
const io::IoSignalCatalog & SignalCatalog() const noexcept
Return the Phoenix PLC's logical signal catalogue.
void Stop()
Stop all active connections.
double DecodeSignal(const io::IoSignal &signal, const Bytes &data) const
Decode one GSDML signal into the connector's double representation.
void Disconnect(const std::string &valveName)
Disconnect a named AUMA connection.
void Connect(asio::io_context &ioContext, InMemoryEthernetBus &bus, VirtualAumaValve &valve, rt::IOCRConfig inputIocr, rt::IOCRConfig outputIocr, int maxConsecutiveTimeouts=cyclic::DEFAULT_MAX_CONSECUTIVE_TIMEOUTS)
Connect the PLC to one virtual AUMA device.
const MacAddress & ControllerMac() const noexcept
Return the PLC MAC address.
std::optional< Bytes > GetLocalInputBytes(std::string_view signalTextId) const
Read raw bytes from the local Phoenix input process image.
std::size_t ConnectionCount() const
Return the number of active AUMA connections.
std::optional< cyclic::CyclicState > ConnectionState(const std::string &valveName) const
Return one connection state.
const io::IoSignal * FindLocalInputSignal(std::string_view textId) const
Return an input signal from the local Phoenix catalogue.
static std::size_t RequiredImageSize(const io::IoSignalCatalog &catalog)
Return the image size required by a GSDML catalogue.
std::string StatusReport() const
Return a human-readable PLC and connection dashboard.
std::vector< std::unique_ptr< Connection > > connections
static const io::IoSignal * FindDeviceSignal(const VirtualAumaValve &valve, std::string_view textId, io::IoSignalDirection direction)
Find a unique signal in a connected device catalogue.
const io::IoSignal * FindLocalOutputSignal(std::string_view textId) const
Return an output signal from the local Phoenix catalogue.
static std::pair< std::uint16_t, std::uint16_t > ResolveDeviceLocation(const io::IoSignal &signal, std::uint16_t defaultSlot, std::uint16_t defaultSubslot)
Find a unique configured slot/subslot for an AUMA signal.
std::optional< Bytes > GetLocalOutputBytes(std::string_view signalTextId) const
Read raw bytes from the local Phoenix output process image.
VirtualPhoenixPlc(std::string name, const gsdml::GSDMLDevice &gsdml, MacAddress controllerMac)
Construct a virtual Phoenix PLC from its parsed GSDML.
std::optional< double > GetLocalInputValue(std::string_view signalTextId) const
Read one typed Phoenix input signal.
Bytes EncodeSignalValue(const io::IoSignal &signal, const Bytes &data, double value) const
Encode one connector double into a device output image.
void SetLocalInputBytes(std::string_view signalTextId, const Bytes &data)
Set raw bytes in the local Phoenix input process image.
std::optional< cyclic::CyclicStatsSnapshot > ConnectionStats(const std::string &valveName) const
Return one connection statistics snapshot.
Connection * FindConnection(const std::string &valveName)
Find a mutable connection by valve name.
~VirtualPhoenixPlc()
Stop all device connections.
std::optional< double > GetLocalOutputValue(std::string_view signalTextId) const
Read one typed Phoenix output signal.
void SetLocalInputValue(std::string_view signalTextId, double value)
Set one typed Phoenix input signal.
std::string ToString(CyclicState state)
Human-readable name for a CyclicState value.
Definition cyclic.cpp:24
@ FloatingPoint
IEEE floating-point value.
@ Boolean
Boolean value, represented by the connector as 0.0 or 1.0.
@ SignedInteger
Signed integer value.
@ UnsignedInteger
Unsigned integer value.
@ Unsupported
Data for which this catalogue does not define a numeric double representation.
IoSignalDirection
Logical direction of a process-data signal from the controller's perspective.
Definition IOSignal.h:30
@ Sensor
Data received from the PROFINET device and therefore exposed as a sensor.
@ Actuator
Data sent to the PROFINET device and therefore exposed as an actuator.
constexpr std::uint16_t ETHERTYPE_PROFINET
EtherType used by PROFINET RT frames (0x8892).
Definition rt.h:34
const char * ToString(VirtualAumaScenario scenario) noexcept
Convert a virtual AUMA scenario into its stable CLI spelling.
std::array< std::uint8_t, macAddressLength > MacAddress
A 6-byte Ethernet MAC address.
Definition util.h:48
std::vector< std::uint8_t > Bytes
Generic byte buffer alias used throughout the library for raw wire data.
Definition protocol.h:55
std::string Mac2String(const MacAddress &mac)
Format a MAC address as a colon-separated string.
Definition util.cpp:188
Complete parsed GSDML device profile.
Definition gsdml.h:166
Describes one logical process-data signal available from a GSDML device.
Definition IOSignal.h:83
bool IsDoubleCompatible() const noexcept
Returns whether this signal can be represented by the IO connector's double field.
Definition IOSignal.cpp:428
std::size_t byteLength
Size of the GSDML DataItem in bytes.
Definition IOSignal.h:150
std::string textId
Original GSDML TextId.
Definition IOSignal.h:91
std::vector< int > possibleSubslots
Subslots in which the containing submodule may occur, when known.
Definition IOSignal.h:160
std::size_t byteOffset
Byte offset within the submodule's input or output process-data area.
Definition IOSignal.h:147
IoSignalValueType valueType
Logical value category inferred from gsdmlDataType.
Definition IOSignal.h:122
std::vector< int > possibleSlots
Slots in which the containing module may occur, when known.
Definition IOSignal.h:155
IOCR configuration derived from AR setup: timing parameters and IO object mappings needed for cyclic ...
Definition rt.h:137
std::vector< IODataObject > objects
IO data objects carried in this IOCR's cyclic frame.
Definition rt.h:163
std::uint16_t dataLength
Total cyclic data length in bytes.
Definition rt.h:160
Bytes inputs
PLC-side input process image.
Bytes outputs
PLC-side output process image.
std::unique_ptr< cyclic::CyclicController > controller
std::unique_ptr< asio::io_context > ioContext
Shared ASIO context.
MockRawEthernetSocket * txSocket
MockRawEthernetSocket * rxSocket
Stateful in-memory AUMA SGx PROFINET IO device simulator.
In-memory Phoenix Contact AXC F 1252 PLC/controller simulator.