29const io::IoSignal* FindSignal(
const io::IoSignalCatalog& catalog,
30 const std::string_view textId,
35 : catalog.Actuators();
36 const io::IoSignal* result =
nullptr;
37 for (
const auto& signal : signals)
39 if (signal.textId != textId)
43 if (result !=
nullptr)
45 throw std::invalid_argument(
"Signal is not unique: " + std::string(textId));
55void ValidateRange(
const io::IoSignal& signal,
const Bytes& data)
57 if (signal.byteOffset > data.size() || signal.byteLength > data.size() - signal.byteOffset)
59 throw std::out_of_range(
"Signal lies outside the process-data object: " + signal.textId);
67std::uint64_t ReadUnsigned(
const Bytes& data,
const io::IoSignal& signal)
69 ValidateRange(signal, data);
72 if (signal.bitOffset >= 8U)
74 throw std::out_of_range(
"Signal bit offset exceeds one byte: " + signal.textId);
76 return (data[signal.byteOffset] &
static_cast<std::uint8_t
>(1U << signal.bitOffset)) != 0U ? 1U : 0U;
79 if (signal.byteLength >
sizeof(std::uint64_t))
81 throw std::invalid_argument(
"Signal is wider than uint64_t: " + signal.textId);
83 std::uint64_t value = 0;
84 for (std::size_t index = 0; index < signal.byteLength; ++index)
86 value = (value << 8U) | data[signal.byteOffset + index];
95void WriteUnsigned(
Bytes& data,
const io::IoSignal& signal,
const std::uint64_t value)
97 ValidateRange(signal, data);
100 if (signal.bitOffset >= 8U || value > 1U)
102 throw std::invalid_argument(
"Invalid Boolean value for signal: " + signal.textId);
104 const auto mask =
static_cast<std::uint8_t
>(1U << signal.bitOffset);
107 data[signal.byteOffset] |= mask;
111 data[signal.byteOffset] &=
static_cast<std::uint8_t
>(~mask);
116 if (signal.byteLength >
sizeof(std::uint64_t))
118 throw std::invalid_argument(
"Signal is wider than uint64_t: " + signal.textId);
120 for (std::size_t index = 0; index < signal.byteLength; ++index)
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);
133 : name(std::move(nameRef)),
135 controllerMac(controllerMacRef),
136 catalog(io::IoSignalCatalog::FromGsdml(this->gsdml))
152 const int maxConsecutiveTimeouts)
154 const std::string connectionName = valve.
Name();
156 std::lock_guard<std::mutex> lock(
mutex);
159 throw std::invalid_argument(
"Phoenix connection already exists: " + connectionName);
164 throw std::invalid_argument(
"Phoenix connection requires non-zero IOCR data lengths");
167 auto connection = std::make_unique<Connection>();
168 connection->name = connectionName;
169 connection->valve = &valve;
173 for (
const auto&
object : outputIocr.
objects)
175 if (
object.slot == 2U &&
object.subslot == 1U &&
object.dataLength > 0U)
177 connection->outputImage.assign(
object.dataLength, 0U);
181 if (connection->outputImage.empty())
183 throw std::invalid_argument(
"Phoenix connection has no slot 2/1 output object");
190 connection->controller = std::make_unique<cyclic::CyclicController>(
195 std::move(inputIocr),
196 std::move(outputIocr),
197 maxConsecutiveTimeouts,
206 std::unique_ptr<Connection> removed;
208 std::lock_guard<std::mutex> lock(
mutex);
210 [&valveName](
const std::unique_ptr<Connection>& connection)
212 return connection != nullptr && connection->name == valveName;
218 removed = std::move(*it);
222 if (removed !=
nullptr && removed->controller !=
nullptr)
224 removed->controller->Stop();
230 std::vector<std::unique_ptr<Connection>> local;
232 std::lock_guard<std::mutex> lock(
mutex);
235 for (
auto& connection : local)
237 if (connection !=
nullptr && connection->controller !=
nullptr)
239 connection->controller->Stop();
245 const std::string_view signalTextId,
248 std::lock_guard<std::mutex> lock(
mutex);
250 if (connection ==
nullptr || connection->
controller ==
nullptr || connection->
valve ==
nullptr)
252 throw std::invalid_argument(
"Unknown Phoenix connection: " + valveName);
257 if (signal ==
nullptr)
259 throw std::invalid_argument(
"Unknown AUMA actuator signal: " + std::string(signalTextId));
270 const std::string_view signalTextId)
const
272 std::lock_guard<std::mutex> lock(
mutex);
274 if (connection ==
nullptr || connection->
controller ==
nullptr || connection->
valve ==
nullptr)
281 if (signal ==
nullptr)
289 const auto data = connection->
controller->GetInputData(slot, subslot);
290 if (!data.has_value())
300 std::lock_guard<std::mutex> lock(
mutex);
302 if (signal ==
nullptr)
304 throw std::invalid_argument(
"Unknown Phoenix input signal: " + std::string(signalTextId));
308 throw std::invalid_argument(
"Phoenix input byte length mismatch for signal: " +
309 std::string(signalTextId));
317 std::lock_guard<std::mutex> lock(
mutex);
319 if (signal ==
nullptr)
331 std::lock_guard<std::mutex> lock(
mutex);
333 if (signal ==
nullptr)
335 throw std::invalid_argument(
"Unknown Phoenix input signal: " + std::string(signalTextId));
342 std::lock_guard<std::mutex> lock(
mutex);
344 if (signal ==
nullptr)
353 std::lock_guard<std::mutex> lock(
mutex);
355 if (signal ==
nullptr)
357 throw std::invalid_argument(
"Unknown Phoenix output signal: " + std::string(signalTextId));
361 throw std::invalid_argument(
"Phoenix output byte length mismatch for signal: " +
362 std::string(signalTextId));
365 std::copy(data.begin(), data.end(),
371 std::lock_guard<std::mutex> lock(
mutex);
373 if (signal ==
nullptr)
385 std::lock_guard<std::mutex> lock(
mutex);
387 if (signal ==
nullptr)
389 throw std::invalid_argument(
"Unknown Phoenix output signal: " + std::string(signalTextId));
396 std::lock_guard<std::mutex> lock(
mutex);
398 if (signal ==
nullptr)
422 std::lock_guard<std::mutex> lock(
mutex);
426std::optional<cyclic::CyclicState>
429 std::lock_guard<std::mutex> lock(
mutex);
431 if (connection ==
nullptr || connection->
controller ==
nullptr)
438std::optional<cyclic::CyclicStatsSnapshot>
441 std::lock_guard<std::mutex> lock(
mutex);
443 if (connection ==
nullptr || connection->
controller ==
nullptr)
447 return connection->
controller->StatsSnapshot();
452 std::lock_guard<std::mutex> lock(
mutex);
453 std::ostringstream out;
455 out <<
" connections: " <<
connections.size() <<
'\n';
458 if (connection ==
nullptr || connection->controller ==
nullptr || connection->valve ==
nullptr)
462 const auto stats = connection->controller->StatsSnapshot();
463 out <<
" - " << connection->name
465 <<
" position=" << connection->valve->Position()
466 <<
" target=" << connection->valve->TargetPosition()
467 <<
" scenario=" <<
ToString(connection->valve->Scenario())
468 <<
" rx=" << stats.framesReceived
469 <<
" missed=" << stats.framesMissed
479 if (connection !=
nullptr && connection->name == valveName)
481 return connection.get();
492 if (connection !=
nullptr && connection->name == valveName)
494 return connection.get();
501 const std::string_view textId,
509 const std::uint64_t raw = ReadUnsigned(data, signal);
513 return raw == 0U ? 0.0 : 1.0;
515 return static_cast<double>(raw);
518 const std::size_t bits = signal.
byteLength * 8U;
519 if (bits == 0U || bits > 64U)
521 throw std::invalid_argument(
"Invalid signed integer width: " + signal.
textId);
525 const std::int64_t signedValue = std::bit_cast<std::int64_t>(raw);
526 return static_cast<double>(signedValue);
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);
538 const std::uint32_t bits =
static_cast<std::uint32_t
>(raw);
540 std::memcpy(&value, &bits,
sizeof(value));
541 return static_cast<double>(value);
546 std::memcpy(&value, &raw,
sizeof(value));
549 throw std::invalid_argument(
"Unsupported floating-point width: " + signal.
textId);
551 throw std::invalid_argument(
"Unsupported Phoenix simulator signal: " + signal.
textId);
553 throw std::logic_error(
"Unknown IoSignalValueType");
558 const double value)
const
563 throw std::invalid_argument(
"Signal is not double-compatible: " + signal.
textId);
565 if (!std::isfinite(value))
567 throw std::invalid_argument(
"Signal value must be finite: " + signal.
textId);
572 if (value != 0.0 && value != 1.0)
574 throw std::invalid_argument(
"Boolean signal requires value 0 or 1: " + signal.
textId);
576 WriteUnsigned(result, signal, value == 0.0 ? 0U : 1U);
582 ValidateRange(signal, result);
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))
588 throw std::out_of_range(
"Float32 signal value is outside its range: " + signal.
textId);
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)
594 const std::size_t shift = (
sizeof(bits) - index - 1U) * 8U;
596 static_cast<std::uint8_t
>((bits >> shift) & 0xFFU);
602 const std::uint64_t bits = std::bit_cast<std::uint64_t>(value);
603 for (std::size_t index = 0; index <
sizeof(bits); ++index)
605 const std::size_t shift = (
sizeof(bits) - index - 1U) * 8U;
607 static_cast<std::uint8_t
>((bits >> shift) & 0xFFU);
611 throw std::invalid_argument(
"Unsupported floating-point width: " + signal.
textId);
614 const long double integerValue =
static_cast<long double>(value);
615 if (std::floor(integerValue) != integerValue)
617 throw std::invalid_argument(
"Integer signal requires an integral value: " + signal.
textId);
621 throw std::invalid_argument(
"Integer signal has an invalid width: " + signal.
textId);
624 const std::size_t bits = signal.
byteLength * 8U;
627 if (integerValue < 0.0L)
629 throw std::invalid_argument(
"Unsigned signal cannot be negative: " + signal.
textId);
631 const long double maxValue = bits == 64U
632 ? 18446744073709551615.0L
633 :
static_cast<long double>((1ULL << bits) - 1ULL);
634 if (integerValue > maxValue)
636 throw std::out_of_range(
"Unsigned signal value exceeds its range: " + signal.
textId);
638 WriteUnsigned(result, signal,
static_cast<std::uint64_t
>(value));
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)
652 throw std::out_of_range(
"Signed signal value exceeds its range: " + signal.
textId);
654 const auto signedValue =
static_cast<std::int64_t
>(value);
655 WriteUnsigned(result, signal, std::bit_cast<std::uint64_t>(signedValue));
659 throw std::invalid_argument(
"Unsupported signal value type: " + signal.
textId);
664 std::size_t size = 0;
665 const auto accumulate = [&size](
const std::vector<io::IoSignal>& signals)
667 for (
const auto& signal : signals)
669 size = std::max(size, signal.byteOffset + signal.byteLength);
672 accumulate(signalCatalog.
Sensors());
689 const std::uint16_t defaultSlot,
690 const std::uint16_t defaultSubslot)
692 std::uint16_t slot = defaultSlot;
693 std::uint16_t subslot = defaultSubslot;
697 signal.
possibleSlots.front() > std::numeric_limits<std::uint16_t>::max())
699 throw std::invalid_argument(
"AUMA signal has ambiguous slot placement: " + signal.
textId);
701 slot =
static_cast<std::uint16_t
>(signal.
possibleSlots.front());
706 signal.
possibleSubslots.front() > std::numeric_limits<std::uint16_t>::max())
708 throw std::invalid_argument(
"AUMA signal has ambiguous subslot placement: " + signal.
textId);
712 return {slot, subslot};
Builds and owns the logical sensor and actuator signals described by a GSDML device.
const std::vector< IoSignal > & Actuators() const noexcept
Returns all non-reserved actuator/output signals.
const std::vector< IoSignal > & Sensors() const noexcept
Returns all non-reserved sensor/input signals.
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.
io::IoSignalCatalog catalog
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.
VirtualPhoenixIoImage ioImage
~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.
@ 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.
@ 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).
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.
std::vector< std::uint8_t > Bytes
Generic byte buffer alias used throughout the library for raw wire data.
std::string Mac2String(const MacAddress &mac)
Format a MAC address as a colon-separated string.
Complete parsed GSDML device profile.
Describes one logical process-data signal available from a GSDML device.
bool IsDoubleCompatible() const noexcept
Returns whether this signal can be represented by the IO connector's double field.
std::size_t byteLength
Size of the GSDML DataItem in bytes.
std::string textId
Original GSDML TextId.
std::vector< int > possibleSubslots
Subslots in which the containing submodule may occur, when known.
std::size_t byteOffset
Byte offset within the submodule's input or output process-data area.
IoSignalValueType valueType
Logical value category inferred from gsdmlDataType.
std::vector< int > possibleSlots
Slots in which the containing module may occur, when known.
IOCR configuration derived from AR setup: timing parameters and IO object mappings needed for cyclic ...
std::vector< IODataObject > objects
IO data objects carried in this IOCR's cyclic frame.
std::uint16_t dataLength
Total cyclic data length in bytes.
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.