20constexpr std::uint8_t kRtaVersion = 1;
21constexpr std::uint8_t kRtaDataPduType = 0x01;
22constexpr std::uint8_t kRtaErrorPduType = 0x0F;
23constexpr std::uint8_t kRtaAdditionalFlags = 0x90;
25constexpr std::uint16_t kMaxPosition = 10000;
33const gsdml::GSDMLSubmodule& RequireSubmodule(
const gsdml::GSDMLDevice& device,
34 const std::string& moduleId,
35 const std::string& submoduleId)
37 const auto*
module = device.FindModule(moduleId);
38 if (module ==
nullptr)
40 throw std::invalid_argument(
"AUMA GSDML is missing module '" + moduleId +
"'");
43 const auto inlineIt =
module->submodules.find(submoduleId);
44 if (inlineIt != module->submodules.end())
46 return inlineIt->second;
49 const auto* submodule = device.FindSubmodule(submoduleId);
50 if (submodule ==
nullptr)
52 throw std::invalid_argument(
"AUMA GSDML is missing submodule '" + submoduleId +
"'");
64 if (
frame.size() < 12U)
68 std::copy_n(
frame.begin(), destination.size(), destination.begin());
69 std::copy_n(
frame.begin() + 6, source.size(), source.begin());
76void ValidateRange(
const io::IoSignal& signal,
const std::size_t dataSize)
78 if (signal.byteOffset > dataSize || signal.byteLength > dataSize - signal.byteOffset)
80 throw std::out_of_range(
"AUMA signal lies outside its process-data object: " + signal.textId);
89const io::IoSignal* FindSignal(
const io::IoSignalCatalog& catalog,
90 const std::string_view textId,
95 : catalog.Actuators();
96 const io::IoSignal* result =
nullptr;
97 for (
const auto& signal : signals)
99 if (signal.textId != textId)
103 if (result !=
nullptr)
105 throw std::invalid_argument(
"AUMA simulator signal is not unique: " + std::string(textId));
126 frame.insert(
frame.end(), destination.begin(), destination.end());
127 frame.insert(
frame.end(), source.begin(), source.end());
135 frame.insert(
frame.end(), rta.begin(), rta.end());
158 return "invalid-input";
160 return "duplicate-input";
162 return "out-of-order-input";
173 : name(std::move(nameRef)),
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)
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");
195 throw std::invalid_argument(
"AUMA initial position must be in range 0..10000");
199 throw std::invalid_argument(
"AUMA travelUnitsPerSecond must be greater than zero");
203 throw std::invalid_argument(
"AUMA slowValveMultiplier must be in (0, 1]");
209 throw std::invalid_argument(
"AUMA IOCR data length is smaller than the GSDML process data");
212 const auto hasInputObject = std::any_of(
217 return object.slot == this->config.inputSlot && object.subslot == this->config.inputSubslot &&
218 object.dataLength == inputSubmodule->inputLength &&
219 object.offsetIOPS < this->inputIocr.dataLength;
221 const auto hasOutputObject = std::any_of(
226 return object.slot == this->config.outputSlot && object.subslot == this->config.outputSubslot &&
227 object.dataLength == outputSubmodule->outputLength;
229 if (!hasInputObject || !hasOutputObject)
231 throw std::invalid_argument(
"AUMA IOCRs do not contain the configured slot 1/1 and 2/1 objects");
234 inputProcessData.resize(
static_cast<std::size_t
>(inputSubmodule->inputLength), 0U);
235 outputProcessData.resize(
static_cast<std::size_t
>(outputSubmodule->outputLength), 0U);
262 socket->SetTimeout(std::chrono::milliseconds(1));
263 worker = std::jthread([
this]
284 socket->SetTimeout(std::chrono::milliseconds(1));
408 auto last = std::chrono::steady_clock::now();
410 auto nextInput = last;
423 const auto now = std::chrono::steady_clock::now();
424 if (now >= nextInput)
426 const double elapsed = std::max(0.0, std::chrono::duration<double>(now - last).count());
433 nextInput = now + cycle;
443 if (!ReadMacs(
frame, destination, source) || destination !=
deviceMac)
450 !parsed->IsValid() || parsed->transferStatus != 0U)
459 object.dataLength == 0U)
463 const std::size_t offset =
object.frameOffset;
464 if (offset > parsed->payload.size() ||
object.dataLength > parsed->payload.size() - offset)
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));
474 if (processData.empty())
508 std::optional<std::uint16_t> setpoint;
516 setpoint =
static_cast<std::uint16_t
>(std::min<std::uint64_t>(raw, kMaxPosition));
526 speed = std::clamp(
static_cast<double>(raw) / 100.0, 0.01, 1.0);
532 if (setpoint.has_value())
537 if (stop || (open && close))
547 if (open && !setpoint.has_value())
551 if (close && !setpoint.has_value())
577 const auto next =
static_cast<std::uint16_t
>(
578 std::min<double>(kMaxPosition,
static_cast<double>(
position) + delta));
583 const auto next =
static_cast<std::uint16_t
>(
584 std::max<double>(0.0,
static_cast<double>(
position) - delta));
612 static_cast<std::uint64_t
>(
moving
620 const auto* signal = FindSignal(
catalog, textId, direction);
621 if (signal ==
nullptr)
623 throw std::invalid_argument(
"AUMA simulator cannot find signal '" + std::string(textId) +
"'");
629 const std::string_view textId,
630 const bool value)
const
633 if (signal ==
nullptr)
637 ValidateRange(*signal, data.size());
638 if (signal->bitSignal)
640 if (signal->bitOffset >= 8U)
642 throw std::out_of_range(
"AUMA bit signal has invalid bit offset: " + signal->textId);
644 const auto mask =
static_cast<std::uint8_t
>(1U << signal->bitOffset);
647 data[signal->byteOffset] |= mask;
651 data[signal->byteOffset] &=
static_cast<std::uint8_t
>(~mask);
655 if (signal->byteLength != 1U)
657 throw std::invalid_argument(
"AUMA Boolean signal is wider than one byte: " + signal->textId);
659 data[signal->byteOffset] = value ? 1U : 0U;
663 const std::string_view textId,
664 const std::uint64_t value)
const
667 if (signal ==
nullptr)
671 ValidateRange(*signal, data.size());
672 if (signal->bitSignal || signal->byteLength == 0U || signal->byteLength >
sizeof(std::uint64_t))
674 throw std::invalid_argument(
"Invalid AUMA scalar signal: " + signal->textId);
677 for (std::size_t index = 0; index < signal->byteLength; ++index)
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);
688 ValidateRange(signal, data.size());
689 if (signal.bitSignal)
691 if (signal.bitOffset >= 8U)
693 throw std::out_of_range(
"AUMA bit signal has invalid bit offset: " + signal.textId);
695 return (data[signal.byteOffset] &
static_cast<std::uint8_t
>(1U << signal.bitOffset)) != 0U;
697 return data[signal.byteOffset] != 0U;
701 const std::string_view textId)
const
704 ValidateRange(signal, data.size());
705 if (signal.bitSignal)
709 if (signal.byteLength == 0U || signal.byteLength >
sizeof(std::uint64_t))
711 throw std::invalid_argument(
"AUMA scalar is outside uint64_t range: " + signal.textId);
714 std::uint64_t value = 0;
715 for (std::size_t index = 0; index < signal.byteLength; ++index)
717 value = (value << 8U) | data[signal.byteOffset + index];
748 object.dataLength == 0U)
752 if (
object.frameOffset >
payload.size() ||
753 object.dataLength >
payload.size() -
object.frameOffset ||
754 object.dataLength > processData.size())
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())
779 std::uint16_t cycleCounter = 0;
798 frame.cycleCounter = cycleCounter;
805 frame.dataStatus =
static_cast<std::uint8_t
>(
frame.dataStatus &
806 ~rt::DATA_STATUS_VALID);
808 frame.transferStatus = 0U;
812 socket->Send(ethernetFrame);
817 const std::uint8_t errorCode2)
const
821 std::uint16_t sequence = 1;
841 rta.push_back(
static_cast<std::uint8_t
>((kRtaVersion << 4U) | kRtaErrorPduType));
842 rta.push_back(kRtaAdditionalFlags);
844 wire::PutU16(rta,
static_cast<std::uint16_t
>(sequence - 1U));
848 return BuildRtaEthernetFrame(destination,
deviceMac, kAlarmFrameId, rta,
true);
888 std::uint8_t errorCode2 = 0x81U;
889 std::uint16_t alarmSpecifier = 0x0001U;
890 switch (localScenario)
894 alarmSpecifier = 0x0801U;
898 alarmSpecifier = 0x0802U;
902 alarmSpecifier = 0x0803U;
906 alarmSpecifier = 0x0804U;
910 alarmSpecifier = 0x0805U;
914 alarmSpecifier = 0x0806U;
918 alarmSpecifier = 0x0807U;
922 alarmSpecifier = 0x0808U;
930 event.errorCode1 = 0xCFU;
931 event.errorCode2 = errorCode2;
932 event.alarmSpecifier = alarmSpecifier;
947 std::uint16_t sequence = 0;
959 rta.push_back(
static_cast<std::uint8_t
>((kRtaVersion << 4U) | kRtaDataPduType));
960 rta.push_back(kRtaAdditionalFlags);
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());
966 event.alarmNotificationFrame =
967 BuildRtaEthernetFrame(destination,
deviceMac, kAlarmFrameId, rta,
true);
977 std::function<void(
const Bytes&)> callback;
Builds and owns the logical sensor and actuator signals described by a GSDML device.
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
io::IoSignalCatalog catalog
std::uint16_t OutputSubslot() const noexcept
Return the configured output subslot.
std::uint16_t targetPosition
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.
rt::IOCRConfig outputIocr
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
std::uint16_t alarmSequence
Bytes InputProcessData() const
Return the most recent input process-data object.
void Stop()
Stop cyclic simulation.
std::uint32_t inputFrameNumber
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.
std::uint16_t rxCycleCounter
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.
std::atomic< bool > running
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.
VirtualAumaScenario scenario
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.
@ 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.
constexpr std::uint8_t IOXS_BAD
IOxS value: bad.
constexpr std::uint8_t DATA_STATUS_STATION_OK
DataStatus bit: station health, 0=Problem, 1=OK.
constexpr std::uint8_t DATA_STATUS_PROVIDER_RUN
DataStatus bit: provider run state, 0=Stop, 1=Run.
Bytes BuildEthernetFrame(const MacAddress &dstMac, const MacAddress &srcMac, const RTFrame &rtFrame)
Build a complete Ethernet frame carrying an RT frame.
constexpr std::uint8_t DATA_STATUS_STATE
DataStatus bit: provider state, 0=Backup, 1=Primary.
constexpr std::uint16_t FRAME_ID_ALARM_LOW
Frame ID used for low-priority alarms.
constexpr std::uint16_t ETHERTYPE_PROFINET
EtherType used by PROFINET RT frames (0x8892).
constexpr std::uint8_t DATA_STATUS_VALID
DataStatus bit: data validity, 0=Invalid, 1=Valid.
constexpr std::uint8_t IOXS_GOOD
IOxS value: good, subslot level.
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.
void PutU32(std::vector< std::uint8_t > &out, std::uint32_t value)
Append a 32-bit value to a buffer in big-endian order.
std::array< std::uint8_t, macAddressLength > MacAddress
A 6-byte Ethernet MAC address.
std::vector< std::uint8_t > Bytes
Generic byte buffer alias used throughout the library for raw wire data.
Complete parsed GSDML device profile.
Describes one logical process-data signal available from a GSDML device.
IOCR configuration derived from AR setup: timing parameters and IO object mappings needed for cyclic ...
std::uint32_t CycleTimeUs() const
Compute the cycle time in microseconds.
std::vector< IODataObject > objects
IO data objects carried in this IOCR's cyclic frame.
std::uint16_t dataLength
Total cyclic data length in bytes.
std::uint16_t sendClockFactor
Send clock base factor (31.25us units).
std::uint16_t frameId
Frame ID assigned to this IOCR.
std::uint16_t reductionRatio
Reduction ratio relative to the send clock.
Single IO data object within the C_SDU: one piece of process data mapped to a slot/subslot,...
A single PROFINET Real-Time cyclic frame: Frame ID + C_SDU payload + cycle counter/status trailer.
std::uint16_t frameId
Frame ID identifying which IOCR this frame belongs to.
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.
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.