PROFINET IO Controller Stack 1.0.0
Modern C++ implementation of a PROFINET IO Controller stack
Loading...
Searching...
No Matches
virtual_auma_valve.cpp
Go to the documentation of this file.
1
3
5
6#include <algorithm>
7#include <cmath>
8#include <stdexcept>
9#include <string>
10#include <string_view>
11
12#include "profinet/wire.h"
13
14namespace profinet::sim
15{
16
17namespace
18{
19
20constexpr std::uint8_t kRtaVersion = 1;
21constexpr std::uint8_t kRtaDataPduType = 0x01;
22constexpr std::uint8_t kRtaErrorPduType = 0x0F;
23constexpr std::uint8_t kRtaAdditionalFlags = 0x90;
24constexpr std::uint16_t kAlarmFrameId = rt::FRAME_ID_ALARM_LOW;
25constexpr std::uint16_t kMaxPosition = 10000;
26
33const gsdml::GSDMLSubmodule& RequireSubmodule(const gsdml::GSDMLDevice& device,
34 const std::string& moduleId,
35 const std::string& submoduleId)
36{
37 const auto* module = device.FindModule(moduleId);
38 if (module == nullptr)
39 {
40 throw std::invalid_argument("AUMA GSDML is missing module '" + moduleId + "'");
41 }
42
43 const auto inlineIt = module->submodules.find(submoduleId);
44 if (inlineIt != module->submodules.end())
45 {
46 return inlineIt->second;
47 }
48
49 const auto* submodule = device.FindSubmodule(submoduleId);
50 if (submodule == nullptr)
51 {
52 throw std::invalid_argument("AUMA GSDML is missing submodule '" + submoduleId + "'");
53 }
54 return *submodule;
55}
56
62bool ReadMacs(const Bytes& frame, MacAddress& destination, MacAddress& source)
63{
64 if (frame.size() < 12U)
65 {
66 return false;
67 }
68 std::copy_n(frame.begin(), destination.size(), destination.begin());
69 std::copy_n(frame.begin() + 6, source.size(), source.begin());
70 return true;
71}
72
76void ValidateRange(const io::IoSignal& signal, const std::size_t dataSize)
77{
78 if (signal.byteOffset > dataSize || signal.byteLength > dataSize - signal.byteOffset)
79 {
80 throw std::out_of_range("AUMA signal lies outside its process-data object: " + signal.textId);
81 }
82}
83
89const io::IoSignal* FindSignal(const io::IoSignalCatalog& catalog,
90 const std::string_view textId,
91 const io::IoSignalDirection direction)
92{
93 const auto& signals = direction == io::IoSignalDirection::Sensor
94 ? catalog.Sensors()
95 : catalog.Actuators();
96 const io::IoSignal* result = nullptr;
97 for (const auto& signal : signals)
98 {
99 if (signal.textId != textId)
100 {
101 continue;
102 }
103 if (result != nullptr)
104 {
105 throw std::invalid_argument("AUMA simulator signal is not unique: " + std::string(textId));
106 }
107 result = &signal;
108 }
109 return result;
110}
111
119Bytes BuildRtaEthernetFrame(const MacAddress& destination,
120 const MacAddress& source,
121 const std::uint16_t frameId,
122 const Bytes& rta,
123 const bool vlan)
124{
125 Bytes frame;
126 frame.insert(frame.end(), destination.begin(), destination.end());
127 frame.insert(frame.end(), source.begin(), source.end());
128 if (vlan)
129 {
130 wire::PutU16(frame, 0x8100U);
131 wire::PutU16(frame, 0xC000U);
132 }
135 frame.insert(frame.end(), rta.begin(), rta.end());
136 return frame;
137}
138
139} // namespace
140
141const char* ToString(const VirtualAumaScenario scenario) noexcept
142{
143 switch (scenario)
144 {
146 return "normal";
148 return "fault";
150 return "bad-iops";
152 return "no-input";
154 return "slow-valve";
156 return "jammed";
158 return "invalid-input";
160 return "duplicate-input";
162 return "out-of-order-input";
163 }
164 return "unknown";
165}
166
168 const gsdml::GSDMLDevice& gsdmlRef,
169 MacAddress deviceMacRef,
170 rt::IOCRConfig inputIocrRef,
171 rt::IOCRConfig outputIocrRef,
173 : name(std::move(nameRef)),
174 gsdml(gsdmlRef),
175 deviceMac(deviceMacRef),
176 inputIocr(std::move(inputIocrRef)),
177 outputIocr(std::move(outputIocrRef)),
178 config(std::move(configRef)),
179 catalog(io::IoSignalCatalog::FromGsdml(this->gsdml)),
180 position(this->config.initialPosition),
181 targetPosition(this->config.initialPosition)
182{
183 const auto* inputSubmodule =
184 &RequireSubmodule(this->gsdml, "ID_MODULE_ADI_IN_0", "ID_SUBMOD_ADI_IN_0");
185 const auto* outputSubmodule =
186 &RequireSubmodule(this->gsdml, "ID_MODULE_ADI_OUT_0", "ID_SUBMOD_ADI_OUT_0");
187
192
193 if (config.initialPosition > kMaxPosition)
194 {
195 throw std::invalid_argument("AUMA initial position must be in range 0..10000");
196 }
197 if (config.travelUnitsPerSecond <= 0.0)
198 {
199 throw std::invalid_argument("AUMA travelUnitsPerSecond must be greater than zero");
200 }
202 {
203 throw std::invalid_argument("AUMA slowValveMultiplier must be in (0, 1]");
204 }
205
206 if (inputIocr.dataLength < static_cast<std::uint16_t>(inputSubmodule->inputLength) ||
207 outputIocr.dataLength < static_cast<std::uint16_t>(outputSubmodule->outputLength))
208 {
209 throw std::invalid_argument("AUMA IOCR data length is smaller than the GSDML process data");
210 }
211
212 const auto hasInputObject = std::any_of(
213 inputIocr.objects.begin(),
214 inputIocr.objects.end(),
215 [this, &inputSubmodule](const rt::IODataObject& object)
216 {
217 return object.slot == this->config.inputSlot && object.subslot == this->config.inputSubslot &&
218 object.dataLength == inputSubmodule->inputLength &&
219 object.offsetIOPS < this->inputIocr.dataLength;
220 });
221 const auto hasOutputObject = std::any_of(
222 outputIocr.objects.begin(),
223 outputIocr.objects.end(),
224 [this, &outputSubmodule](const rt::IODataObject& object)
225 {
226 return object.slot == this->config.outputSlot && object.subslot == this->config.outputSubslot &&
227 object.dataLength == outputSubmodule->outputLength;
228 });
229 if (!hasInputObject || !hasOutputObject)
230 {
231 throw std::invalid_argument("AUMA IOCRs do not contain the configured slot 1/1 and 2/1 objects");
232 }
233
234 inputProcessData.resize(static_cast<std::size_t>(inputSubmodule->inputLength), 0U);
235 outputProcessData.resize(static_cast<std::size_t>(outputSubmodule->outputLength), 0U);
237}
238
243
245{
246 if (running.exchange(true))
247 {
248 return;
249 }
250
251 try
252 {
253 {
254 std::lock_guard<std::mutex> lock(stateMutex);
255 controllerKnown = false;
256 controllerMac = {};
257 rxCycleCounter = 0U;
258 inputFrameNumber = 0U;
259 alarmSequence = 1U;
260 }
262 socket->SetTimeout(std::chrono::milliseconds(1));
263 worker = std::jthread([this]
264 {
265 Run();
266 });
267 }
268 catch (...)
269 {
270 running = false;
271 socket.reset();
272 throw;
273 }
274}
275
277{
278 if (!running.exchange(false))
279 {
280 return;
281 }
282 if (socket != nullptr)
283 {
284 socket->SetTimeout(std::chrono::milliseconds(1));
285 }
286 if (worker.joinable())
287 {
288 worker.join();
289 }
290 socket.reset();
291}
292
294{
295 VirtualAumaScenario oldScenario;
296 {
297 std::lock_guard<std::mutex> lock(stateMutex);
298 oldScenario = scenario;
299 scenario = newScenario;
300 if (newScenario == VirtualAumaScenario::Fault || newScenario == VirtualAumaScenario::Jammed)
301 {
302 moving = false;
303 }
304 if (newScenario == VirtualAumaScenario::Normal &&
306 {
307 moving = true;
308 }
309 }
310
311 if (newScenario != oldScenario && newScenario != VirtualAumaScenario::Normal)
312 {
313 EmitScenarioAlarms(newScenario);
314 }
315}
316
318{
319 std::lock_guard<std::mutex> lock(stateMutex);
320 return scenario;
321}
322
323std::uint16_t VirtualAumaValve::Position() const noexcept
324{
325 std::lock_guard<std::mutex> lock(stateMutex);
326 return position;
327}
328
329std::uint16_t VirtualAumaValve::TargetPosition() const noexcept
330{
331 std::lock_guard<std::mutex> lock(stateMutex);
332 return targetPosition;
333}
334
335bool VirtualAumaValve::FaultActive() const noexcept
336{
337 std::lock_guard<std::mutex> lock(stateMutex);
339}
340
341bool VirtualAumaValve::Moving() const noexcept
342{
343 std::lock_guard<std::mutex> lock(stateMutex);
344 return moving;
345}
346
347const std::string& VirtualAumaValve::Name() const noexcept
348{
349 return name;
350}
351
353{
354 return deviceMac;
355}
356
357std::uint16_t VirtualAumaValve::InputSlot() const noexcept
358{
359 return config.inputSlot;
360}
361
362std::uint16_t VirtualAumaValve::InputSubslot() const noexcept
363{
364 return config.inputSubslot;
365}
366
367std::uint16_t VirtualAumaValve::OutputSlot() const noexcept
368{
369 return config.outputSlot;
370}
371
372std::uint16_t VirtualAumaValve::OutputSubslot() const noexcept
373{
374 return config.outputSubslot;
375}
376
378{
379 return catalog;
380}
381
382void VirtualAumaValve::OnFrame(std::function<void(const Bytes&)> callback)
383{
384 std::lock_guard<std::mutex> lock(stateMutex);
385 onFrame = std::move(callback);
386}
387
388void VirtualAumaValve::OnAlarm(std::function<void(const VirtualAumaAlarmEvent&)> callback)
389{
390 std::lock_guard<std::mutex> lock(stateMutex);
391 onAlarm = std::move(callback);
392}
393
395{
396 std::lock_guard<std::mutex> lock(stateMutex);
397 return inputProcessData;
398}
399
401{
402 std::lock_guard<std::mutex> lock(stateMutex);
403 return outputProcessData;
404}
405
407{
408 auto last = std::chrono::steady_clock::now();
409 const auto cycle = std::chrono::microseconds(inputIocr.CycleTimeUs());
410 auto nextInput = last;
411
412 while (running)
413 {
414 if (socket != nullptr)
415 {
416 const Bytes frame = socket->Recv();
417 if (!frame.empty())
418 {
420 }
421 }
422
423 const auto now = std::chrono::steady_clock::now();
424 if (now >= nextInput)
425 {
426 const double elapsed = std::max(0.0, std::chrono::duration<double>(now - last).count());
427 last = now;
428 UpdateProcessModel(elapsed);
430 nextInput += cycle;
431 if (nextInput < now)
432 {
433 nextInput = now + cycle;
434 }
435 }
436 }
437}
438
440{
441 MacAddress destination{};
442 MacAddress source{};
443 if (!ReadMacs(frame, destination, source) || destination != deviceMac)
444 {
445 return;
446 }
447
448 const auto parsed = rt::ParseEthernetFrame(frame);
449 if (!parsed.has_value() || parsed->frameId != outputIocr.frameId ||
450 !parsed->IsValid() || parsed->transferStatus != 0U)
451 {
452 return;
453 }
454
455 Bytes processData;
456 for (const auto& object : outputIocr.objects)
457 {
458 if (object.slot != config.outputSlot || object.subslot != config.outputSubslot ||
459 object.dataLength == 0U)
460 {
461 continue;
462 }
463 const std::size_t offset = object.frameOffset;
464 if (offset > parsed->payload.size() || object.dataLength > parsed->payload.size() - offset)
465 {
466 return;
467 }
468 processData.assign(parsed->payload.begin() + static_cast<std::ptrdiff_t>(offset),
469 parsed->payload.begin() +
470 static_cast<std::ptrdiff_t>(offset + object.dataLength));
471 break;
472 }
473
474 if (processData.empty())
475 {
476 return;
477 }
478
479 {
480 std::lock_guard<std::mutex> lock(stateMutex);
481 if (controllerKnown && controllerMac != source)
482 {
483 return;
484 }
485 controllerMac = source;
486 controllerKnown = true;
487 outputProcessData = processData;
488 }
489
490 if ((parsed->dataStatus & rt::DATA_STATUS_PROVIDER_RUN) == 0U)
491 {
492 std::lock_guard<std::mutex> lock(stateMutex);
493 opening = false;
494 closing = false;
495 moving = false;
496 return;
497 }
498
499 DecodeCommands(processData);
500}
501
503{
504 const bool open = ReadBitSignal(outputData, config.openCommand);
505 const bool close = ReadBitSignal(outputData, config.closeCommand);
506 const bool stop = ReadBitSignal(outputData, config.stopCommand);
507
508 std::optional<std::uint16_t> setpoint;
510 {
511 const auto raw = ReadScalarSignal(outputData, config.positionSetpoint);
512 // Treat a non-zero setpoint as an explicit absolute target. Zero remains
513 // the neutral/default value so OPEN/CLOSE still mean end-stop travel.
514 if (raw != 0U)
515 {
516 setpoint = static_cast<std::uint16_t>(std::min<std::uint64_t>(raw, kMaxPosition));
517 }
518 }
519
520 double speed = 1.0;
521 if (FindSignal(catalog, config.speedSetpoint, io::IoSignalDirection::Actuator) != nullptr)
522 {
523 const auto raw = ReadScalarSignal(outputData, config.speedSetpoint);
524 if (raw != 0U)
525 {
526 speed = std::clamp(static_cast<double>(raw) / 100.0, 0.01, 1.0);
527 }
528 }
529
530 std::lock_guard<std::mutex> lock(stateMutex);
531 commandSpeed = speed;
532 if (setpoint.has_value())
533 {
534 targetPosition = *setpoint;
535 }
536
537 if (stop || (open && close))
538 {
539 opening = false;
540 closing = false;
541 moving = false;
542 return;
543 }
544
545 opening = open;
546 closing = close;
547 if (open && !setpoint.has_value())
548 {
549 targetPosition = kMaxPosition;
550 }
551 if (close && !setpoint.has_value())
552 {
553 targetPosition = 0U;
554 }
557}
558
559void VirtualAumaValve::UpdateProcessModel(const double elapsedSeconds)
560{
561 std::lock_guard<std::mutex> lock(stateMutex);
562
564 {
565 moving = false;
567 return;
568 }
569
570 const double multiplier = scenario == VirtualAumaScenario::SlowValve
572 : 1.0;
573 const double delta = config.travelUnitsPerSecond * commandSpeed * multiplier * elapsedSeconds;
574
575 if (opening)
576 {
577 const auto next = static_cast<std::uint16_t>(
578 std::min<double>(kMaxPosition, static_cast<double>(position) + delta));
579 position = next;
580 }
581 else if (closing)
582 {
583 const auto next = static_cast<std::uint16_t>(
584 std::max<double>(0.0, static_cast<double>(position) - delta));
585 position = next;
586 }
587
588 if (position == targetPosition || (!opening && !closing))
589 {
590 moving = false;
591 }
593}
594
596{
597 std::fill(inputProcessData.begin(), inputProcessData.end(), 0U);
598
599 const bool fault = scenario == VirtualAumaScenario::Fault ||
601 const bool warning = scenario == VirtualAumaScenario::SlowValve;
602
606 SetBitSignal(inputProcessData, "T_ID_OPERATIONSTATUS_0", opening);
607 SetBitSignal(inputProcessData, "T_ID_OPERATIONSTATUS_1", closing);
608 SetBitSignal(inputProcessData, "T_ID_OPERATIONSTATUS_2", moving);
609 SetBitSignal(inputProcessData, "T_ID_OPERATIONSTATUS_3", position == 0U);
610 SetBitSignal(inputProcessData, "T_ID_OPERATIONSTATUS_4", position == kMaxPosition);
612 static_cast<std::uint64_t>(moving
613 ? std::clamp(commandSpeed * 100.0, 1.0, 100.0)
614 : 0.0));
615}
616
617const io::IoSignal& VirtualAumaValve::RequireSignal(const std::string_view textId,
618 const io::IoSignalDirection direction) const
619{
620 const auto* signal = FindSignal(catalog, textId, direction);
621 if (signal == nullptr)
622 {
623 throw std::invalid_argument("AUMA simulator cannot find signal '" + std::string(textId) + "'");
624 }
625 return *signal;
626}
627
629 const std::string_view textId,
630 const bool value) const
631{
632 const auto* signal = FindSignal(catalog, textId, io::IoSignalDirection::Sensor);
633 if (signal == nullptr)
634 {
635 return;
636 }
637 ValidateRange(*signal, data.size());
638 if (signal->bitSignal)
639 {
640 if (signal->bitOffset >= 8U)
641 {
642 throw std::out_of_range("AUMA bit signal has invalid bit offset: " + signal->textId);
643 }
644 const auto mask = static_cast<std::uint8_t>(1U << signal->bitOffset);
645 if (value)
646 {
647 data[signal->byteOffset] |= mask;
648 }
649 else
650 {
651 data[signal->byteOffset] &= static_cast<std::uint8_t>(~mask);
652 }
653 return;
654 }
655 if (signal->byteLength != 1U)
656 {
657 throw std::invalid_argument("AUMA Boolean signal is wider than one byte: " + signal->textId);
658 }
659 data[signal->byteOffset] = value ? 1U : 0U;
660}
661
663 const std::string_view textId,
664 const std::uint64_t value) const
665{
666 const auto* signal = FindSignal(catalog, textId, io::IoSignalDirection::Sensor);
667 if (signal == nullptr)
668 {
669 return;
670 }
671 ValidateRange(*signal, data.size());
672 if (signal->bitSignal || signal->byteLength == 0U || signal->byteLength > sizeof(std::uint64_t))
673 {
674 throw std::invalid_argument("Invalid AUMA scalar signal: " + signal->textId);
675 }
676
677 for (std::size_t index = 0; index < signal->byteLength; ++index)
678 {
679 const std::size_t shift = (signal->byteLength - 1U - index) * 8U;
680 data[signal->byteOffset + index] =
681 static_cast<std::uint8_t>((value >> shift) & 0xFFU);
682 }
683}
684
685bool VirtualAumaValve::ReadBitSignal(const Bytes& data, const std::string_view textId) const
686{
687 const auto& signal = RequireSignal(textId, io::IoSignalDirection::Actuator);
688 ValidateRange(signal, data.size());
689 if (signal.bitSignal)
690 {
691 if (signal.bitOffset >= 8U)
692 {
693 throw std::out_of_range("AUMA bit signal has invalid bit offset: " + signal.textId);
694 }
695 return (data[signal.byteOffset] & static_cast<std::uint8_t>(1U << signal.bitOffset)) != 0U;
696 }
697 return data[signal.byteOffset] != 0U;
698}
699
701 const std::string_view textId) const
702{
703 const auto& signal = RequireSignal(textId, io::IoSignalDirection::Actuator);
704 ValidateRange(signal, data.size());
705 if (signal.bitSignal)
706 {
707 return ReadBitSignal(data, textId) ? 1U : 0U;
708 }
709 if (signal.byteLength == 0U || signal.byteLength > sizeof(std::uint64_t))
710 {
711 throw std::invalid_argument("AUMA scalar is outside uint64_t range: " + signal.textId);
712 }
713
714 std::uint64_t value = 0;
715 for (std::size_t index = 0; index < signal.byteLength; ++index)
716 {
717 value = (value << 8U) | data[signal.byteOffset + index];
718 }
719 return value;
720}
721
723{
724 VirtualAumaScenario localScenario;
725 MacAddress destination{};
726 Bytes processData;
727 {
728 std::lock_guard<std::mutex> lock(stateMutex);
729 localScenario = scenario;
730 processData = inputProcessData;
731 if (!controllerKnown)
732 {
733 return;
734 }
735 destination = controllerMac;
736 }
737
738 if (localScenario == VirtualAumaScenario::NoInput || socket == nullptr)
739 {
740 return;
741 }
742
744 bool mapped = false;
745 for (const auto& object : inputIocr.objects)
746 {
747 if (object.slot != config.inputSlot || object.subslot != config.inputSubslot ||
748 object.dataLength == 0U)
749 {
750 continue;
751 }
752 if (object.frameOffset > payload.size() ||
753 object.dataLength > payload.size() - object.frameOffset ||
754 object.dataLength > processData.size())
755 {
756 return;
757 }
758 std::copy(processData.begin(),
759 processData.begin() + static_cast<std::ptrdiff_t>(object.dataLength),
760 payload.begin() + object.frameOffset);
761 if (object.offsetIOPS < payload.size())
762 {
763 payload[*object.offsetIOPS] =
764 (localScenario == VirtualAumaScenario::BadIops ||
765 localScenario == VirtualAumaScenario::Fault)
768 }
769 mapped = true;
770 break;
771 }
772 if (!mapped)
773 {
774 return;
775 }
776
777 const std::uint32_t step = static_cast<std::uint32_t>(inputIocr.sendClockFactor) *
779 std::uint16_t cycleCounter = 0;
781 {
782 std::lock_guard<std::mutex> lock(stateMutex);
784 counterScenario = scenario;
785 if (counterScenario != VirtualAumaScenario::DuplicateInput)
786 {
787 rxCycleCounter = static_cast<std::uint16_t>((rxCycleCounter + step) & 0xFFFFU);
788 }
789 if (counterScenario == VirtualAumaScenario::OutOfOrderInput && inputFrameNumber % 4U == 0U)
790 {
791 rxCycleCounter = static_cast<std::uint16_t>((rxCycleCounter - (2U * step)) & 0xFFFFU);
792 }
793 cycleCounter = rxCycleCounter;
794 }
795
798 frame.cycleCounter = cycleCounter;
799 frame.dataStatus = static_cast<std::uint8_t>(rt::DATA_STATUS_VALID |
803 if (localScenario == VirtualAumaScenario::InvalidInput)
804 {
805 frame.dataStatus = static_cast<std::uint8_t>(frame.dataStatus &
806 ~rt::DATA_STATUS_VALID);
807 }
808 frame.transferStatus = 0U;
809 frame.payload = std::move(payload);
810
811 const Bytes ethernetFrame = rt::BuildEthernetFrame(destination, deviceMac, frame);
812 socket->Send(ethernetFrame);
813 NotifyFrame(ethernetFrame);
814}
815
816Bytes VirtualAumaValve::BuildRtaErrorFrame(const std::uint8_t errorCode1,
817 const std::uint8_t errorCode2) const
818{
819 MacAddress destination{};
820 std::uint16_t destinationReference = config.alarmDestinationReference;
821 std::uint16_t sequence = 1;
822 {
823 std::lock_guard<std::mutex> lock(stateMutex);
824 destination = controllerMac;
825 sequence = alarmSequence;
826 }
827
828 if (!controllerKnown)
829 {
830 return {};
831 }
832
834 wire::PutU16(varPart, 0x0000U);
835 varPart.push_back(errorCode1);
836 varPart.push_back(errorCode2);
837
838 Bytes rta;
839 wire::PutU16(rta, destinationReference);
841 rta.push_back(static_cast<std::uint8_t>((kRtaVersion << 4U) | kRtaErrorPduType));
842 rta.push_back(kRtaAdditionalFlags);
843 wire::PutU16(rta, sequence);
844 wire::PutU16(rta, static_cast<std::uint16_t>(sequence - 1U));
845 wire::PutU16(rta, static_cast<std::uint16_t>(varPart.size()));
846 rta.insert(rta.end(), varPart.begin(), varPart.end());
847
848 return BuildRtaEthernetFrame(destination, deviceMac, kAlarmFrameId, rta, true);
849}
850
851Bytes VirtualAumaValve::BuildAlarmNotificationBlock(const std::uint16_t alarmSpecifier) const
852{
853 Bytes body;
854 wire::PutU16(body, 0x0001U);
860 wire::PutU16(body, alarmSpecifier);
861
862 // Add a standard channel-diagnosis item. This makes the generated alarm
863 // useful for both AlarmNotification parsing and diagnosis handling tests.
864 Bytes item;
865 wire::PutU16(item, 0x8000U);
866 wire::PutU16(item, 1U);
867 wire::PutU16(item, 0x0800U);
868 wire::PutU16(item, 9U);
869
870 Bytes payload{0x01U, 0x00U};
871 payload.insert(payload.end(), body.begin(), body.end());
872 payload.insert(payload.end(), item.begin(), item.end());
873
874 Bytes block;
875 wire::PutU16(block, 0x0002U);
876 wire::PutU16(block, static_cast<std::uint16_t>(payload.size()));
877 block.insert(block.end(), payload.begin(), payload.end());
878 return block;
879}
880
882{
883 if (socket == nullptr)
884 {
885 return;
886 }
887
888 std::uint8_t errorCode2 = 0x81U;
889 std::uint16_t alarmSpecifier = 0x0001U;
890 switch (localScenario)
891 {
893 errorCode2 = 0x81U;
894 alarmSpecifier = 0x0801U;
895 break;
897 errorCode2 = 0x82U;
898 alarmSpecifier = 0x0802U;
899 break;
901 errorCode2 = 0x83U;
902 alarmSpecifier = 0x0803U;
903 break;
905 errorCode2 = 0x84U;
906 alarmSpecifier = 0x0804U;
907 break;
909 errorCode2 = 0x85U;
910 alarmSpecifier = 0x0805U;
911 break;
913 errorCode2 = 0x86U;
914 alarmSpecifier = 0x0806U;
915 break;
917 errorCode2 = 0x87U;
918 alarmSpecifier = 0x0807U;
919 break;
921 errorCode2 = 0x88U;
922 alarmSpecifier = 0x0808U;
923 break;
925 return;
926 }
927
929 event.scenario = localScenario;
930 event.errorCode1 = 0xCFU;
931 event.errorCode2 = errorCode2;
932 event.alarmSpecifier = alarmSpecifier;
933
935 {
936 event.rtaErrorFrame = BuildRtaErrorFrame(event.errorCode1, event.errorCode2);
937 if (!event.rtaErrorFrame.empty())
938 {
939 socket->Send(event.rtaErrorFrame);
941 }
942 }
943
945 {
946 MacAddress destination{};
947 std::uint16_t sequence = 0;
948 {
949 std::lock_guard<std::mutex> lock(stateMutex);
950 destination = controllerMac;
951 sequence = alarmSequence;
953 }
954
955 const Bytes block = BuildAlarmNotificationBlock(alarmSpecifier);
956 Bytes rta;
959 rta.push_back(static_cast<std::uint8_t>((kRtaVersion << 4U) | kRtaDataPduType));
960 rta.push_back(kRtaAdditionalFlags);
961 wire::PutU16(rta, sequence);
962 wire::PutU16(rta, static_cast<std::uint16_t>(sequence - 1U));
963 wire::PutU16(rta, static_cast<std::uint16_t>(block.size()));
964 rta.insert(rta.end(), block.begin(), block.end());
965
966 event.alarmNotificationFrame =
967 BuildRtaEthernetFrame(destination, deviceMac, kAlarmFrameId, rta, true);
968 socket->Send(event.alarmNotificationFrame);
970 }
971
972 NotifyAlarm(event);
973}
974
976{
977 std::function<void(const Bytes&)> callback;
978 {
979 std::lock_guard<std::mutex> lock(stateMutex);
980 callback = onFrame;
981 }
982 if (callback)
983 {
984 try
985 {
986 callback(frame);
987 }
988 catch (...)
989 {
990 // A simulator callback must never terminate the device worker thread.
991 }
992 }
993}
994
996{
997 std::function<void(const VirtualAumaAlarmEvent&)> callback;
998 {
999 std::lock_guard<std::mutex> lock(stateMutex);
1000 callback = onAlarm;
1001 }
1002 if (callback)
1003 {
1004 try
1005 {
1006 callback(event);
1007 }
1008 catch (...)
1009 {
1010 // A simulator callback must never terminate the device worker thread.
1011 }
1012 }
1013}
1014
1015} // namespace profinet::sim
Builds and owns the logical sensor and actuator signals described by a GSDML device.
Definition IOSignal.h:186
Small virtual Ethernet switch suitable for deterministic tests and demos.
std::unique_ptr< InMemoryEthernetSocket > CreateSocket(MacAddress localMac)
Create one virtual raw-Ethernet endpoint.
const MacAddress & DeviceMac() const noexcept
Return the simulated device MAC address.
const gsdml::GSDMLDevice & gsdml
std::uint16_t OutputSubslot() const noexcept
Return the configured output subslot.
Bytes OutputProcessData() const
Return the most recent output process-data object.
const std::string & Name() const noexcept
Return the configured simulator name.
void SetScenario(VirtualAumaScenario scenario)
Set the current simulator scenario.
void SendInputFrame()
Send one device-to-controller RTC1 input frame.
VirtualAumaScenario Scenario() const noexcept
Return the current scenario.
std::uint16_t InputSlot() const noexcept
Return the configured input slot.
VirtualAumaValve(std::string name, const gsdml::GSDMLDevice &gsdml, MacAddress deviceMac, rt::IOCRConfig inputIocr, rt::IOCRConfig outputIocr, VirtualAumaValveConfiguration config={})
Construct a virtual AUMA valve.
void ProcessOutputFrame(const Bytes &frame)
Decode and apply one controller RTC1 output frame.
void SetBitSignal(Bytes &data, std::string_view textId, bool value) const
Set one Boolean input signal.
std::uint64_t ReadScalarSignal(const Bytes &data, std::string_view textId) const
Read a big-endian unsigned output signal.
std::function< void(const Bytes &)> onFrame
VirtualAumaValveConfiguration config
std::function< void(const VirtualAumaAlarmEvent &)> onAlarm
Bytes InputProcessData() const
Return the most recent input process-data object.
void Stop()
Stop cyclic simulation.
void DecodeCommands(const Bytes &outputData)
Decode command bits and optional setpoints from an output object.
void OnAlarm(std::function< void(const VirtualAumaAlarmEvent &)> callback)
Register a callback for generated scenario alarm events.
void Run()
Main worker loop receiving controller outputs and sending device inputs.
void NotifyFrame(const Bytes &frame) const
Invoke the frame callback outside the state mutex.
const io::IoSignalCatalog & SignalCatalog() const noexcept
Return the GSDML-derived signal catalogue.
bool FaultActive() const noexcept
Return whether an application or mechanical fault is active.
std::unique_ptr< InMemoryEthernetSocket > socket
void Start(InMemoryEthernetBus &bus)
Attach the device to a shared virtual Ethernet bus and start cyclic processing.
Bytes BuildAlarmNotificationBlock(std::uint16_t alarmSpecifier) const
Build a PROFINET AlarmNotification block for a scenario.
std::uint16_t OutputSlot() const noexcept
Return the configured output slot.
bool Moving() const noexcept
Return whether the valve model is moving toward its target.
std::uint16_t InputSubslot() const noexcept
Return the configured input subslot.
Bytes BuildRtaErrorFrame(std::uint8_t errorCode1, std::uint8_t errorCode2) const
Build the raw RTA 0x0F scenario error frame.
const io::IoSignal & RequireSignal(std::string_view textId, io::IoSignalDirection direction) const
Resolve a unique GSDML signal or throw a configuration error.
void UpdateProcessModel(double elapsedSeconds)
Advance the valve application model for one elapsed interval.
bool ReadBitSignal(const Bytes &data, std::string_view textId) const
Read a Boolean output signal.
std::uint16_t TargetPosition() const noexcept
Return the current requested target position.
void BuildInputProcessData()
Rebuild all AUMA input measurement and status signals.
std::uint16_t Position() const noexcept
Return the current simulated position.
void EmitScenarioAlarms(VirtualAumaScenario scenario)
Emit enabled scenario alarms and notify callbacks.
void OnFrame(std::function< void(const Bytes &)> callback)
Register a callback for every Ethernet frame emitted by the device.
void SetScalarSignal(Bytes &data, std::string_view textId, std::uint64_t value) const
Set one big-endian unsigned input signal.
void NotifyAlarm(const VirtualAumaAlarmEvent &event) const
Invoke the alarm callback outside the state mutex.
~VirtualAumaValve()
Stop the simulator and release the virtual transport.
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.
std::optional< RTFrame > ParseEthernetFrame(const Bytes &data)
Parse a complete Ethernet frame and extract its RT frame.
Definition rt.cpp:261
constexpr std::uint8_t IOXS_BAD
IOxS value: bad.
Definition rt.h:101
constexpr std::uint8_t DATA_STATUS_STATION_OK
DataStatus bit: station health, 0=Problem, 1=OK.
Definition rt.h:89
constexpr std::uint8_t DATA_STATUS_PROVIDER_RUN
DataStatus bit: provider run state, 0=Stop, 1=Run.
Definition rt.h:87
Bytes BuildEthernetFrame(const MacAddress &dstMac, const MacAddress &srcMac, const RTFrame &rtFrame)
Build a complete Ethernet frame carrying an RT frame.
Definition rt.cpp:242
constexpr std::uint8_t DATA_STATUS_STATE
DataStatus bit: provider state, 0=Backup, 1=Primary.
Definition rt.h:79
constexpr std::uint16_t FRAME_ID_ALARM_LOW
Frame ID used for low-priority alarms.
Definition rt.h:63
constexpr std::uint16_t ETHERTYPE_PROFINET
EtherType used by PROFINET RT frames (0x8892).
Definition rt.h:34
constexpr std::uint8_t DATA_STATUS_VALID
DataStatus bit: data validity, 0=Invalid, 1=Valid.
Definition rt.h:83
constexpr std::uint8_t IOXS_GOOD
IOxS value: good, subslot level.
Definition rt.h:95
VirtualAumaScenario
Deterministic operating and communication scenarios for a virtual AUMA valve.
@ Fault
Application fault: movement stops, IOPS becomes BAD and alarms are emitted.
@ BadIops
Input frames continue, but the input provider reports BAD IOPS.
@ DuplicateInput
Input frames repeat their previous cycle counter.
@ SlowValve
Valve travels using the configured reduced speed multiplier.
@ OutOfOrderInput
Input frames occasionally move the cycle counter backwards.
@ Normal
Fully healthy operation with normal travel speed.
@ NoInput
Input frames stop completely so the controller watchdog expires.
@ InvalidInput
Input frames remain valid but carry an invalid RTC1 DATA_STATUS.
@ Jammed
Valve accepts commands but the mechanical position does not change.
const char * ToString(VirtualAumaScenario scenario) noexcept
Convert a virtual AUMA scenario into its stable CLI spelling.
void PutU16(std::vector< std::uint8_t > &out, std::uint16_t value)
Append a 16-bit value to a buffer in big-endian order.
Definition wire.h:97
void PutU32(std::vector< std::uint8_t > &out, std::uint32_t value)
Append a 32-bit value to a buffer in big-endian order.
Definition wire.h:105
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
Complete parsed GSDML device profile.
Definition gsdml.h:166
Describes one logical process-data signal available from a GSDML device.
Definition IOSignal.h:83
IOCR configuration derived from AR setup: timing parameters and IO object mappings needed for cyclic ...
Definition rt.h:137
std::uint32_t CycleTimeUs() const
Compute the cycle time in microseconds.
Definition rt.h:171
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
std::uint16_t sendClockFactor
Send clock base factor (31.25us units).
Definition rt.h:148
std::uint16_t frameId
Frame ID assigned to this IOCR.
Definition rt.h:145
std::uint16_t reductionRatio
Reduction ratio relative to the send clock.
Definition rt.h:151
Single IO data object within the C_SDU: one piece of process data mapped to a slot/subslot,...
Definition rt.h:109
A single PROFINET Real-Time cyclic frame: Frame ID + C_SDU payload + cycle counter/status trailer.
Definition rt.h:208
std::uint16_t frameId
Frame ID identifying which IOCR this frame belongs to.
Definition rt.h:210
Captured alarm traffic produced by a virtual AUMA scenario.
Bytes rtaErrorFrame
Complete RTA 0x0F Ethernet frame; empty when disabled.
VirtualAumaScenario scenario
Scenario that caused this event.
std::uint8_t errorCode1
First simulator-defined RTA error code byte.
std::uint8_t errorCode2
Second simulator-defined RTA error code byte.
Bytes alarmNotificationFrame
Complete RTA DATA AlarmNotification Ethernet frame; empty when disabled.
Application-level signal mapping and motion settings for a virtual AUMA SGx.
std::string speedSetpoint
Optional speed setpoint signal, interpreted as 0..100 percent.
std::uint32_t alarmModuleIdent
Module identifier used by the simulated input alarm.
std::string faultSignal
Generic fault indication signal.
std::uint32_t alarmSubmoduleIdent
Submodule identifier used by the simulated input alarm.
std::string positionSetpoint
Optional position setpoint signal.
std::string openCommand
OPEN command bit in the AUMA output process data.
bool emitAlarmNotification
Whether a parseable AlarmNotification is emitted for scenarios.
std::uint16_t alarmSourceReference
Source RTA alarm endpoint reference.
std::string stopCommand
STOP command bit in the AUMA output process data.
bool emitRtaError
Whether an RTA 0x0F error PDU is emitted for scenarios.
std::string actualPosition
Actual position sensor signal.
double slowValveMultiplier
Speed multiplier for the SlowValve scenario.
std::string closeCommand
CLOSE command bit in the AUMA output process data.
std::uint32_t alarmApi
AlarmNotification API used by the simulated device.
double travelUnitsPerSecond
Nominal travel speed in position units per second.
std::uint16_t alarmDestinationReference
Destination RTA alarm endpoint reference.
std::string warningSignal
Generic warning indication signal.
std::uint16_t initialPosition
Initial valve position in the AUMA 0..10000 application range.
std::uint16_t outputSubslot
Output process-data subslot.
std::uint16_t inputSlot
Input process-data slot.
std::uint16_t outputSlot
Output process-data slot.
std::uint16_t inputSubslot
Input process-data subslot.
std::uint16_t frameId
Ethernet FrameID.
Bytes varPart
RTA variable part.
Bytes frame
Bytes payload
Definition test_dcp.cpp:392
Stateful in-memory AUMA SGx PROFINET IO device simulator.
Generic helpers for reading and writing integer and byte data in big- and little-endian wire formats.