Reported as samples sporadically carrying a previous packet's timestamp: holes on one side of the stream and collisions on the other, in both the Go and the MARTe2 receiver. That it appeared in both is what located it -- the shared cause is upstream of either client. Four independent defects, all of which end in a packet's values being placed at a time that is not theirs. Reassembly slot exhaustion (the "Reassembly slots full; evicting oldest" flood). Chunk size was learnt only from fragment 0, so an out-of-order burst destroyed a packet whose bytes had all arrived and left the slot occupied until the 2 s GC. Slots were keyed on the counter alone, but DATA and CONFIG number independently, so equal counters merged the two streams. The 32-byte received-mask covered 256 of the 512 fragments the client accepts, so a duplicate above 255 was counted as new and the packet was delivered with a hole of stale bytes in it. And one datagram was read per Execute(), which cannot drain a fast producer. Fixed with a pendingTail deferral, (counter, type) keying, a 64-byte mask, a 256-datagram drain, counter-age slot reclamation, and a 1 Hz aggregated warning in place of the per-eviction flood. UDPStreamer dropping whole Accumulate batches. EventSem::ResetWait is Reset-then-Wait, so a Post() landing while the sender thread was inside ServiceClients()/SendData() was destroyed by the next Reset. The batch was then skipped with dataReady false, readyFill was never cleared, and the following flush overwrote it: an entire run of RT cycles never reached the wire. The record of pending work now lives in the buffers rather than in the semaphore edge, which also removes up to UDPS_DATA_WAIT_MS of latency; genuine backpressure overwrites are counted and reported. Against the unfixed code the new test sees 2999/3000 batches never consumed. Period inflation after loss. Accumulated scalars carry no SamplingRate, so the receiver derives dt from the sender-clock gap -- but dividing it by the previous packet's sample count is only right while nothing is lost. One loss doubles the reported period, which spreads a batch a full batch past its own end and into the range the next packet claims. That is the hole and the collision, exactly. Inferring the cycle count from the estimate's own period is not a way out: it has a stable fixed point wherever gap/dt is an integer, so a real rate change locks it at the old one for good (AccumDtGTest.FollowsSustainedRateChange). The packet counter removes the ambiguity, so all three receivers now order on it: a DATA packet that does not advance the counter is dropped rather than delivered, because its values are older than data already handed over. Ordering is on the signed difference so it survives the uint32 wrap, and the sequence resets on reconnect, where the producer's counter restarts independently of ours. The loss count that falls out of the same delta feeds the period estimate as cycles = prevN * (1 + lost), which reduces exactly to gap/prevN when nothing is lost and therefore still tracks a genuine rate change. UDPSClient::AcceptDataCounter (C++), udpsprotocol.SequenceGate (Go), decode_data (C). The C client's existing gap counter was wrap-unsafe and let a stale packet rewind last_counter, which made every subsequent gap wrong; it uses the same code now. Docs/Protocol.md gains an Ordering DATA section stating the requirement for any receiver, including ones outside this repository. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
433 lines
21 KiB
C++
433 lines
21 KiB
C++
/**
|
||
* @file UDPStreamer.h
|
||
* @brief Header file for class UDPStreamer
|
||
* @date 13/05/2026
|
||
* @author Martino Ferrari
|
||
*
|
||
* @copyright Copyright 2015 F4E | European Joint Undertaking for ITER and
|
||
* the Development of Fusion Energy ('Fusion for Energy').
|
||
* Licensed under the EUPL, Version 1.1 or - as soon they will be approved
|
||
* by the European Commission - subsequent versions of the EUPL (the "Licence")
|
||
* You may not use this work except in compliance with the Licence.
|
||
* You may obtain a copy of the Licence at: http://ec.europa.eu/idabc/eupl
|
||
*
|
||
* @warning Unless required by applicable law or agreed to in writing,
|
||
* software distributed under the Licence is distributed on an "AS IS"
|
||
* basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
|
||
* or implied. See the Licence permissions and limitations under the Licence.
|
||
*
|
||
* @details This header file contains the declaration of the class UDPStreamer
|
||
* with all of its public, protected and private members. It may also include
|
||
* definitions for inline methods which need to be visible to the compiler.
|
||
*/
|
||
|
||
#ifndef UDPSTREAMER_H_
|
||
#define UDPSTREAMER_H_
|
||
|
||
/*---------------------------------------------------------------------------*/
|
||
/* Standard header includes */
|
||
/*---------------------------------------------------------------------------*/
|
||
|
||
/*---------------------------------------------------------------------------*/
|
||
/* Project header includes */
|
||
/*---------------------------------------------------------------------------*/
|
||
#include "CompilerTypes.h"
|
||
#include "EmbeddedServiceMethodBinderI.h"
|
||
#include "EventSem.h"
|
||
#include "FastPollingMutexSem.h"
|
||
#include "MemoryDataSourceI.h"
|
||
#include "SingleThreadService.h"
|
||
#include "StreamString.h"
|
||
#include "UDPSServer.h"
|
||
|
||
/*---------------------------------------------------------------------------*/
|
||
/* Class declaration */
|
||
/*---------------------------------------------------------------------------*/
|
||
|
||
namespace MARTe {
|
||
|
||
/**
|
||
* @brief Publishing mode for the background sender thread.
|
||
*
|
||
* - Strict: send one packet every Synchronise() call (default).
|
||
* - Accumulate: buffer N successive snapshots and flush either when the next
|
||
* snapshot would exceed MaxPayloadSize or when 1/MinRefreshRate
|
||
* has elapsed. Scalars expand to arrays; the first scalar with
|
||
* Unit="us"/"ns" is auto-promoted as the FullArray time reference.
|
||
* - Decimate: send one packet every Ratio Synchronise() calls.
|
||
*/
|
||
typedef enum {
|
||
UDPStreamerPublishStrict = 0u, /**< Send on every RT cycle (default) */
|
||
UDPStreamerPublishAccumulate = 1u, /**< Accumulate until size/time limit; then flush */
|
||
UDPStreamerPublishDecimate = 2u /**< Send 1 packet every Ratio cycles */
|
||
} UDPStreamerPublishMode;
|
||
|
||
/**
|
||
* @brief Quantization types for float signals.
|
||
*/
|
||
typedef enum {
|
||
UDPStreamerQuantNone = 0u, /**< No quantization; raw wire format */
|
||
UDPStreamerQuantUint8 = 1u, /**< Quantize to uint8 [0, 255] */
|
||
UDPStreamerQuantInt8 = 2u, /**< Quantize to int8 [-127, 127] */
|
||
UDPStreamerQuantUint16 = 3u, /**< Quantize to uint16 [0, 65535] */
|
||
UDPStreamerQuantInt16 = 4u /**< Quantize to int16 [-32767, 32767] */
|
||
} UDPStreamerQuantType;
|
||
|
||
/**
|
||
* @brief Time reference modes for signals.
|
||
*/
|
||
typedef enum {
|
||
UDPStreamerTimePacket = 0u, /**< Use the packet-level timestamp (HRT at Synchronise time) */
|
||
UDPStreamerTimeFullArray = 1u, /**< Time signal has same NumberOfElements; one timestamp per element */
|
||
UDPStreamerTimeFirstSample = 2u, /**< Time signal is scalar = timestamp of first element */
|
||
UDPStreamerTimeLastSample = 3u /**< Time signal is scalar = timestamp of last element */
|
||
} UDPStreamerTimeMode;
|
||
|
||
/**
|
||
* @brief Per-signal metadata used at runtime for serialization.
|
||
*/
|
||
struct UDPStreamerSignalInfo {
|
||
StreamString name; /**< Signal name */
|
||
TypeDescriptor type; /**< MARTe2 type descriptor */
|
||
UDPStreamerQuantType quantType; /**< Quantization type (none = raw copy) */
|
||
uint8 numDimensions; /**< Number of dimensions (0=scalar, 1=array, 2=matrix) */
|
||
uint32 numElements; /**< Total number of elements (rows * cols) */
|
||
uint32 numRows; /**< Number of rows */
|
||
uint32 numCols; /**< Number of columns */
|
||
float64 rangeMin; /**< Min of physical range (for quantization) */
|
||
float64 rangeMax; /**< Max of physical range (for quantization) */
|
||
UDPStreamerTimeMode timeMode; /**< How time is encoded for this signal */
|
||
float64 samplingRate; /**< Hz, used for First/LastSample modes */
|
||
uint32 timeSignalIdx; /**< Index of the time-reference signal; 0xFFFFFFFFu = PacketTime */
|
||
StreamString unit; /**< Physical unit string */
|
||
uint32 srcByteSize; /**< Bytes in MARTe2 memory */
|
||
uint32 wireByteSize; /**< Bytes on the wire (may differ when quantized) */
|
||
uint32 bufferOffset; /**< Byte offset in the flat MemoryDataSourceI memory buffer */
|
||
bool accumulated; /**< True when this signal is batched (one snapshot per RT cycle) in Accumulate mode */
|
||
};
|
||
|
||
/**
|
||
* @brief A DataSource that streams MARTe2 signals to UDP clients.
|
||
*
|
||
* @details This output DataSource accepts signals from GAMs and forwards them
|
||
* asynchronously over the network. A dedicated background thread handles all
|
||
* network I/O so that the real-time thread is only blocked by a fast spinlock
|
||
* and a memcpy during Synchronise().
|
||
*
|
||
* Two operating modes are selected by the presence or absence of MulticastGroup:
|
||
*
|
||
* @par Unicast mode (default — no MulticastGroup)
|
||
* The server opens a single UDP socket on Port. The client initiates the session
|
||
* by sending a CONNECT packet to that port. The server replies with a CONFIG
|
||
* packet on the same socket and subsequently sends DATA packets directly to the
|
||
* client's address. Any number of clients may connect sequentially (one at a
|
||
* time); a new CONNECT evicts the previous client.
|
||
*
|
||
* @par Multicast mode (MulticastGroup specified)
|
||
* The server opens a TCP listener on Port for control traffic and a UDP socket
|
||
* aimed at MulticastGroup:DataPort for data traffic. The client:
|
||
* 1. Connects to Port via TCP and sends a CONNECT packet.
|
||
* 2. Receives the CONFIG packet over TCP.
|
||
* 3. Joins the multicast group (MulticastGroup:DataPort) to receive DATA packets.
|
||
* Multiple clients may receive data simultaneously by joining the same group.
|
||
* A new TCP CONNECT evicts the previous client from the control channel; the
|
||
* multicast data stream continues regardless.
|
||
*
|
||
* @par Packet protocol (both modes)
|
||
* - Client → Server: CONNECT (initiates session), DISCONNECT (terminates session),
|
||
* ACK (optional, for packet-loss monitoring).
|
||
* - Server → Client: CONFIG (signal metadata), DATA (signal values, possibly
|
||
* fragmented into multiple datagrams if payload exceeds MaxPayloadSize).
|
||
*
|
||
* @par Top-level configuration parameters
|
||
* | Parameter | Type | Default | Description |
|
||
* |-----------------|---------|------------------|-------------|
|
||
* | Port | uint16 | 44500 | TCP control port (multicast) or UDP server port (unicast). Values ≤ 1024 produce a warning. |
|
||
* | MulticastGroup | string | *(absent)* | **Enables multicast mode.** IPv4 multicast address, e.g. `"239.0.0.1"`. Must be in 224.0.0.0/4. Absent or empty = unicast. |
|
||
* | Interface | string | *(absent)* | Multicast binded interface **ONLY FOR MULTICAST** |
|
||
* | DataPort | uint16 | Port+1 | UDP port for multicast DATA datagrams. Ignored in unicast mode. Must be non-zero and differ from Port. |
|
||
* | MaxPayloadSize | uint32 | 1400 | Maximum bytes of signal payload per UDP datagram (excluding the 17-byte header). Larger signals are fragmented. |
|
||
* | PublishingMode | string | Strict | `Strict`: send one packet every Synchronise() call. `Auto`: rate-limited; flush only when MinRefreshRate interval has elapsed. |
|
||
* | MinRefreshRate | float64 | — | Required when PublishingMode = Auto. Flush frequency in Hz (e.g. 120.0). |
|
||
* | MaxBatchSize | uint32 | 1 | Optional when PublishingMode = Auto. Number of RT cycles to accumulate before flushing one packet. Scalar signals are expanded to arrays of MaxBatchSize elements; the first scalar with Unit="us" or "ns" is auto-promoted as the per-sample FullArray timestamp reference for all other scalars. When omitted or 1, the most-recent single value is sent at MinRefreshRate. |
|
||
* | CPUMask | uint32 | 0xFFFFFFFF | CPU affinity bitmask for the background thread. |
|
||
* | StackSize | uint32 | (MARTe2 default) | Stack size in bytes for the background thread. |
|
||
*
|
||
* @par Per-signal configuration parameters
|
||
* | Parameter | Type | Default | Description |
|
||
* |------------------|---------|-------------|-------------|
|
||
* | Type | string | — | MARTe2 type name (uint8, int16, float32, float64, …). Mandatory. |
|
||
* | NumberOfDimensions | uint8 | 0 | 0 = scalar, 1 = array, 2 = matrix. |
|
||
* | NumberOfElements | uint32 | 1 | Total element count (rows × cols for matrices). |
|
||
* | Unit | string | "" | Physical unit label, e.g. `"Pa"` or `"m/s"`. Transmitted in CONFIG for display purposes. |
|
||
* | RangeMin | float64 | 0.0 | Minimum of the physical range. Required when QuantizedType is not `none`. |
|
||
* | RangeMax | float64 | 1.0 | Maximum of the physical range. Required when QuantizedType is not `none`. |
|
||
* | QuantizedType | string | none | Wire quantization for float signals: `none` \| `uint8` \| `int8` \| `uint16` \| `int16`. Reduces wire bandwidth at the cost of precision. Only valid for float32/float64 signals. |
|
||
* | TimeMode | string | PacketTime | How the signal's time axis is encoded (see below). |
|
||
* | TimeSignal | string | — | Name of the signal that carries timestamps. Required when TimeMode ≠ PacketTime. |
|
||
* | SamplingRate | float64 | — | Signal sampling rate in Hz. Required when TimeMode = FirstSample or LastSample. |
|
||
*
|
||
* @par TimeMode values
|
||
* | Value | Description |
|
||
* |--------------|-------------|
|
||
* | PacketTime | Uses the HRT counter captured at Synchronise() time as the single packet timestamp. No dedicated time signal needed. |
|
||
* | FullArray | TimeSignal has the same NumberOfElements as this signal; one timestamp per element. |
|
||
* | FirstSample | TimeSignal is a scalar = timestamp of the first element; subsequent elements are spaced by 1/SamplingRate. |
|
||
* | LastSample | TimeSignal is a scalar = timestamp of the last element; elements are spaced backwards by 1/SamplingRate. |
|
||
*
|
||
* @par Example — unicast mode
|
||
* <pre>
|
||
* +Streamer = {
|
||
* Class = UDPStreamer
|
||
* Port = 44500
|
||
* MaxPayloadSize = 1400
|
||
* PublishingMode = "Strict"
|
||
* Signals = {
|
||
* Time = {
|
||
* Type = uint64
|
||
* }
|
||
* Pressure = {
|
||
* Type = float32
|
||
* NumberOfDimensions = 1
|
||
* NumberOfElements = 100
|
||
* Unit = "Pa"
|
||
* RangeMin = 0.0
|
||
* RangeMax = 1000000.0
|
||
* QuantizedType = uint16
|
||
* TimeMode = LastSample
|
||
* TimeSignal = Time
|
||
* SamplingRate = 10000.0
|
||
* }
|
||
* Temperature = {
|
||
* Type = float64
|
||
* Unit = "degC"
|
||
* TimeMode = PacketTime
|
||
* }
|
||
* }
|
||
* }
|
||
* </pre>
|
||
*
|
||
* @par Example — multicast mode
|
||
* <pre>
|
||
* +Streamer = {
|
||
* Class = UDPStreamer
|
||
* Port = 44500 // TCP control port
|
||
* MulticastGroup = "239.0.0.1" // Enables multicast mode
|
||
* DataPort = 44501 // UDP data port (default: Port+1)
|
||
* MaxPayloadSize = 1400
|
||
* PublishingMode = "Auto"
|
||
* MinRefreshRate = 60
|
||
* Signals = {
|
||
* Time = {
|
||
* Type = uint64
|
||
* }
|
||
* Voltage = {
|
||
* Type = float32
|
||
* NumberOfDimensions = 1
|
||
* NumberOfElements = 1000
|
||
* Unit = "V"
|
||
* RangeMin = -10.0
|
||
* RangeMax = 10.0
|
||
* QuantizedType = int16
|
||
* TimeMode = FirstSample
|
||
* TimeSignal = Time
|
||
* SamplingRate = 100000.0
|
||
* }
|
||
* }
|
||
* }
|
||
* </pre>
|
||
*/
|
||
class UDPStreamer : public MemoryDataSourceI, public EmbeddedServiceMethodBinderI {
|
||
public:
|
||
CLASS_REGISTER_DECLARATION()
|
||
|
||
/**
|
||
* @brief Constructor. Initialises all members to safe defaults.
|
||
*/
|
||
UDPStreamer();
|
||
|
||
/**
|
||
* @brief Destructor. Stops the background thread and frees allocated memory.
|
||
*/
|
||
virtual ~UDPStreamer();
|
||
|
||
/**
|
||
* @brief Parses top-level configuration parameters.
|
||
* @details Reads Port, MaxPayloadSize, CPUMask, StackSize, PublishingMode,
|
||
* MinRefreshRate, MulticastGroup, and DataPort from the configuration node.
|
||
* Sets useMulticast and dataPort when MulticastGroup is present and valid.
|
||
* @return true if all mandatory parameters are valid and consistent.
|
||
*/
|
||
virtual bool Initialise(StructuredDataI &data);
|
||
|
||
/**
|
||
* @brief Validates signal types and builds the per-signal metadata array.
|
||
* @details Reads per-signal custom fields (Unit, RangeMin, RangeMax, QuantizedType,
|
||
* TimeMode, TimeSignal, SamplingRate) from signalsDatabase and populates signalInfos[].
|
||
* @return true if all signal configurations are valid.
|
||
*/
|
||
virtual bool SetConfiguredDatabase(StructuredDataI &data);
|
||
|
||
/**
|
||
* @brief Allocates signal memory (via parent) plus readyBuffer, scratchBuffer, wireBuffer.
|
||
* @return true if all memory allocations succeed.
|
||
*/
|
||
virtual bool AllocateMemory();
|
||
|
||
/**
|
||
* @brief Returns the broker name for the given signal direction.
|
||
* @return "MemoryMapSynchronisedOutputBroker" for OutputSignals; "" otherwise.
|
||
*/
|
||
virtual const char8 *GetBrokerName(StructuredDataI &data,
|
||
const SignalDirection direction);
|
||
|
||
/**
|
||
* @brief Called by the RT thread after all GAMs have written output signals.
|
||
* @details Copies signal memory to readyBuffer under a fast spinlock, then posts
|
||
* the dataSem to wake the background thread. Returns immediately.
|
||
* @return true always.
|
||
*/
|
||
virtual bool Synchronise();
|
||
|
||
/**
|
||
* @brief Opens the server socket and starts the background thread.
|
||
* @return true if the socket and thread start successfully.
|
||
*/
|
||
virtual bool PrepareNextState(const char8 *const currentStateName,
|
||
const char8 *const nextStateName);
|
||
|
||
/**
|
||
* @brief Background thread entry point.
|
||
* @details Handles client CONNECT/DISCONNECT/ACK and sends DATA packets.
|
||
*/
|
||
virtual ErrorManagement::ErrorType Execute(ExecutionInfo &info);
|
||
|
||
/**
|
||
* @brief Returns the listening port number.
|
||
*/
|
||
uint16 GetPort() const;
|
||
|
||
/**
|
||
* @brief Returns the maximum payload size per UDP datagram.
|
||
*/
|
||
uint32 GetMaxPayloadSize() const;
|
||
|
||
/**
|
||
* @brief Returns true if a client is currently connected.
|
||
*/
|
||
bool IsClientConnected() const;
|
||
|
||
/**
|
||
* @brief Returns true when multicast mode is active (MulticastGroup was set in config).
|
||
*/
|
||
bool IsMulticast() const;
|
||
|
||
/**
|
||
* @brief Number of publications the sender thread never put on the wire.
|
||
* @details Synchronise() promotes a snapshot (Strict/Decimate) or a batch
|
||
* (Accumulate) to the ready buffer for the sender thread. If the next
|
||
* promotion arrives before the sender has taken the previous one, that
|
||
* publication is overwritten and its cycles never reach any receiver —
|
||
* which a consumer sees as a hole in the time series. Counts those, so the
|
||
* loss is measurable rather than inferred from the plot.
|
||
*/
|
||
uint32 GetDroppedPublications() const;
|
||
|
||
private:
|
||
/**
|
||
* @brief Serializes the CONFIG payload into buf and sets payloadSize.
|
||
* @return true if serialization succeeds.
|
||
*/
|
||
bool BuildConfigPayload(uint8 *buf, uint32 bufSize, uint32 &payloadSize);
|
||
|
||
/**
|
||
* @brief Quantizes and serializes all signals from srcBuf into wireBuffer.
|
||
* @param[in] srcBuf Flat source buffer (signal data in MARTe2 format).
|
||
* @param[in] timestamp HRT counter to embed as packet timestamp.
|
||
*/
|
||
void QuantizeAndSerialize(const uint8 *srcBuf, uint64 timestamp);
|
||
|
||
/**
|
||
* @brief Serializes an accumulated batch into wireBuffer.
|
||
* @param src Flat buffer [numSamples × totalSrcBytes], slot 0 = oldest.
|
||
* @param timestamps HRT counters per slot; timestamps[0] is the packet timestamp.
|
||
* @param numSamples Actual number of filled slots to serialize.
|
||
*/
|
||
void SerializeAccumulated(const uint8 *src, const uint64 *timestamps, uint32 numSamples);
|
||
|
||
/**
|
||
* @brief Maps a MARTe2 TypeDescriptor to the UDPS_TYPECODE_* constants.
|
||
*/
|
||
static uint8 TypeDescriptorToCode(TypeDescriptor td);
|
||
|
||
/**
|
||
* @brief True when the ready buffer holds data the sender has not taken yet.
|
||
* @details Read under bufMutex. The sender must consult this rather than
|
||
* rely on the dataSem edge, which ResetWait can destroy.
|
||
*/
|
||
bool HasPendingPublication();
|
||
|
||
/**
|
||
* @brief Emits at most one warning per second about overwritten publications.
|
||
*/
|
||
void ReportDroppedPublications();
|
||
|
||
/* Configuration parameters */
|
||
uint16 port; /**< UDP server port */
|
||
uint32 maxPayloadSize; /**< Max payload bytes per UDP packet (excluding header) */
|
||
uint32 cpuMask; /**< Background thread CPU affinity */
|
||
uint32 stackSize; /**< Background thread stack size */
|
||
UDPStreamerPublishMode publishMode; /**< Strict or Auto publishing mode */
|
||
float64 minRefreshRate; /**< Minimum flush rate (Hz) for Auto mode */
|
||
uint64 flushPeriodTicks; /**< HRT ticks per flush interval (computed from minRefreshRate) */
|
||
/* Accumulate mode — dynamic batch parameters */
|
||
uint32 maxBatchCount; /**< Max snapshots that fit in MaxPayloadSize (Accumulate) */
|
||
uint32 singleCycleWireBytes; /**< Wire bytes for ALL signals (scalar and array) per snapshot */
|
||
volatile uint64 lastPublishTs; /**< HRT counter of last successful flush (Accumulate mode) */
|
||
uint8 *accumBuffer; /**< Heap: [maxBatchCount × totalSrcBytes] linear fill */
|
||
uint64 *accumTimestamps; /**< Heap: [maxBatchCount] HRT counter per snapshot */
|
||
uint32 accumFill; /**< Slots filled in accumBuffer (0..maxBatchCount) */
|
||
uint64 *readyTimestamps; /**< Heap: [maxBatchCount] HRT for completed ready batch */
|
||
uint64 *scratchTimestamps; /**< Heap: [maxBatchCount] background-thread local copy */
|
||
uint32 readyFill; /**< Snapshot count in the ready batch */
|
||
/** Strict/Decimate: readyBuffer holds a snapshot the sender has not taken
|
||
* yet. Publication state must live here rather than in dataSem, because
|
||
* EventSem::ResetWait resets before waiting and so destroys any Post that
|
||
* landed while the sender was busy. */
|
||
bool readySnapshotPending;
|
||
uint32 droppedPublications; /**< Publications overwritten before being sent */
|
||
uint64 lastDropReportTicks; /**< Sender-thread rate limit for the drop warning */
|
||
/* Decimate mode */
|
||
uint32 decimateRatio; /**< Send 1 packet every decimateRatio Synchronise() calls */
|
||
uint32 decimateCounter; /**< Current decimate cycle counter */
|
||
|
||
/* Signal metadata */
|
||
uint32 numSigs; /**< Number of signals */
|
||
UDPStreamerSignalInfo *signalInfos; /**< Per-signal metadata array */
|
||
|
||
/* Double-buffering for RT safety */
|
||
uint8 *readyBuffer; /**< Protected copy of signal memory for background thread */
|
||
uint8 *scratchBuffer; /**< Local copy used during serialization (avoids holding lock) */
|
||
uint32 totalSrcBytes; /**< Total bytes of signal data (= stateMemorySize) */
|
||
FastPollingMutexSem bufMutex; /**< Protects readyBuffer against concurrent access */
|
||
EventSem dataSem; /**< Wakes background thread when new data is ready */
|
||
volatile uint64 syncTimestamp; /**< HRT counter captured in Synchronise() */
|
||
|
||
/* Wire serialization buffer */
|
||
uint8 *wireBuffer; /**< Pre-allocated buffer for the serialized DATA payload */
|
||
uint32 totalWireBytes; /**< Total bytes of all signals after quantization + 8-byte timestamp prefix */
|
||
|
||
/* Networking — handled by UDPSServer */
|
||
UDPSServer server; /**< Multi-client session manager (unicast and multicast) */
|
||
|
||
/* Thread management */
|
||
SingleThreadService executor; /**< Background thread service */
|
||
|
||
/* Packet sequencing */
|
||
uint32 packetCounter; /**< Incremented for each DATA update sent */
|
||
};
|
||
|
||
} /* namespace MARTe */
|
||
|
||
#endif /* UDPSTREAMER_H_ */
|