10 Commits
Author SHA1 Message Date
Martino FerrariandClaude Opus 4.6 892e3eae28 fix(udpscope): bound accumulated-burst chaining so packet loss cannot displace the trace
Forward-chaining each accumulated burst onto the previous one suppresses
arrival jitter, but an unchecked chain never recovers: one lost datagram, or a
declared sampling rate that differs from the producer's real one, dates every
later sample early for the rest of the run. The chain is now a prediction,
compared each packet against the arrival anchor and abandoned beyond
kBurstResyncThresholdS, which bounds the error instead of accumulating it.

Plan amended so the hrt-fit fallback (unusable here: the fit needs 32 packets
and is itself corrupted by bursty arrivals) cannot come back.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-08-27 20:08:05 +02:00
Martino FerrariandClaude Sonnet 4.6 5a8479cda9 feat(udpscope): per-element timestamp reconstruction from UDPS frames
Implements FrameDecoder with five timing rules that mirror
UDPSourceSession.cpp: FullArray (per-element time signal), FirstSample
and LastSample (rate-spread from anchor), accumulated scalar with
declared rate (forward-chain anchoring, immune to arrival jitter), and
PACKET burst (backward-span from previous arrival). 9 new tests, 38
pass total.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-27 20:01:30 +02:00
Martino FerrariandClaude Opus 4.6 c89decef8e docs: say that HrtRateFit::toSeconds returns producer-epoch, not wall, seconds
The fit keeps the slope and discards the intercept, so the result counts from
the producer's boot. Tasks 4 and 7 compose it with ClockOffset::map, which is
correct, but the bare name invites passing it straight to a plot axis.

Also unwrapped the stalled-clock assertion from behind `if (fit.ready())` —
that branch never runs, so the test confirmed nothing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-08-27 19:55:24 +02:00
Martino FerrariandClaude Opus 4.6 e4817dd284 fix(udpscope): keep the clock-offset recalibration threshold symmetric
The jitter test fed a receive timestamp that went backwards (1001.02 then
1000.97), putting the second reading 1.03 s from the prediction — twice the
threshold, so not jitter under any reading. That is a digit slip for 1001.97.

It had been worked around by making the threshold one-sided, which passes the
test but never fires when the producer's clock steps forward: the prediction
stays ahead of the wall clock, the error stays negative, and the trace sits in
the future for the rest of the run. Restored std::fabs, corrected the test data,
and added the forward-jump case that the one-sided version silently failed.

Plan amended so the bad data does not come back.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-08-27 19:52:41 +02:00
Martino FerrariandClaude Sonnet 4.6 41ab151a2f feat(udpscope): producer-clock calibration and hrt tick-rate fit
Adds TimeBase.h/cpp with ClockOffset (latched wall-clock offset with
one-sided recalibration on positive drift only, so early-arriving packets
do not wobble the trace) and HrtRateFit (sliding-window OLS that recovers
an unknown hrt tick rate from receive timestamps). Also adds
TimeSignalScale() which maps UDPS type codes to seconds-per-count.
9 new tests, all 28 pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-27 19:50:24 +02:00
Martino FerrariandClaude Opus 4.6 ea9689591d test: add coverage for closeLeaf's two untested branches
Gap 1: closeLeaf was only tested closing the first sibling; added test
closing the second sibling to cover the else branch of parent->a.get()==leaf.
The test verifies the correct sibling survives with its signal intact.

Gap 2: closeLeaf was only tested at depth 1 (root's direct children);
added test with depth-2 leaf (in right subtree) to exercise findParent's
recursive search in both subtrees. Tests that the correct leaf is promoted
and remaining signals are preserved.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-08-27 19:46:29 +02:00
Martino FerrariandClaude Sonnet 4.6 0e5d103e73 feat(udpscope): BSP pane tree with split, close and hit-testing
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-27 19:42:09 +02:00
Martino FerrariandClaude Opus 4.6 c1029a25df fix(udpscope): make the time-order test actually exercise the swap, guard the font copy
The ramp data in EmitsPointsInTimeOrder never produced a bucket whose maximum
preceded its minimum, so an implementation ordering the emitted pair by value
instead of by time would have passed. Replaced with an explicit two-bucket case
whose second bucket reverses the order.

file(COPY) is a hard configure error on a missing source, so a checkout without
the sibling StreamHub resources failed to configure despite the ASCII-icon
fallback the block above had just selected.

Plan amended to match on both points.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-08-27 19:39:14 +02:00
Martino FerrariandClaude Sonnet 4.6 fba4360c80 feat(udpscope): build scaffold and min/max envelope decimation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-27 17:27:41 +02:00
Martino FerrariandClaude Opus 4.6 2d62e1808b docs: fix six cross-task defects found in the UDPScope plan pre-flight
Each of these would have surfaced as a compile/link failure or a reviewer
rejection mid-execution, when the implementer holding the task has no view of
the neighbouring task that contradicts it.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-08-27 17:07:08 +02:00
141 changed files with 2598 additions and 17109 deletions

No files matched your search

+5 -42
View File
@@ -117,16 +117,9 @@ Sent when the signal set changes or a client connects:
``` ```
[uint32 numSigs] [uint32 numSigs]
numSigs × UDPSSignalDescriptor (136 bytes each, packed) numSigs × UDPSSignalDescriptor (136 bytes each, packed)
[uint8 publishMode] 0=Strict, 1=Accumulate, 2=Decimate [uint8 publishMode] 0=Strict/Decimate, 1=Accumulate
[uint64 hrtFrequency] producer's HRT ticks per second; 0 = unknown
``` ```
Everything after the descriptors is an optional trailer: a receiver accepts a
payload that stops early and ignores bytes it does not know. `hrtFrequency` is
what lets a receiver on another host turn the raw counter in DATA into seconds
— without it the only option is the receiver's own timer, which agrees with the
producer only when the two share a machine.
### DATA Payload (Strict / Decimate modes) ### DATA Payload (Strict / Decimate modes)
``` ```
@@ -137,9 +130,9 @@ per-signal data in CONFIG order (quantised or raw, no inter-signal padding)
### DATA Payload (Accumulate mode) ### DATA Payload (Accumulate mode)
``` ```
[uint64 HRT timestamp of the first slot in the batch] [uint64 HRT timestamp]
[uint32 numSamples] RT cycles accumulated into this packet [uint32 numSamples]
for each signal, in CONFIG order: numSamples × NumElements values for each signal: if scalar → numSamples elements; else → NumElements once
``` ```
### Quantization / Dequantization ### Quantization / Dequantization
@@ -246,32 +239,6 @@ DebugServiceI *svc = DebugServiceI::GetInstance(); // broker wrapper retrieves i
5. Applies quantization (`QuantizedType`, `RangeMin`, `RangeMax`) per signal 5. Applies quantization (`QuantizedType`, `RangeMin`, `RangeMax`) per signal
6. Supports unicast and multicast UDP delivery (configurable per instance) 6. Supports unicast and multicast UDP delivery (configurable per instance)
### TCP variant
`TCPStreamer` / `TCPStreamerClient` subclass the UDP DataSources and replace the transport
(`TCPSServer` / `TCPSClient`) behind the `UDPSServerI` / `UDPSClientI` seam
(`UDPSTransportI.h`). Framing is unchanged (`totalFragments` is always 1); each client has
a bounded queue, so a slow client drops only its own frames.
`TCPStreamService` (`Source/Components/Interfaces/TCPStreamService/`) is an optional
in-process server: one acceptor plus `Workers` I/O threads serve many `TCPSServiceStream`s
(one per `TCPStreamer` with `Service=`/`StreamName=`). Producers only copy frames into
per-connection queues and wake the worker via a self-pipe. Lock order: topology, stream,
connection. A client picks streams with a `SUBSCRIBE` frame (type 5):
- **Multiplexing**: the payload may be a comma separated list; each server frame carries the
stream's list index in `fragmentIdx`; queues are accounted per stream. `TCPSClient`
demultiplexes into per-stream listeners (`GetStream(i)` facades), and StreamHub creates
one session per stream over one shared client.
- **Joining**: `TCPStreamJoin` taps several streams (`TCPSStreamTapI`), watches the service
for (un)registration (`TCPSStreamObserverI`) and publishes a merged Strict stream: CONFIG =
concatenated descriptors named `<stream>_<signal>`; one DATA frame per master frame with
the other sources held at their last value. No extra thread. Accumulate sources and
differing HRT frequencies are refused. Lock order: source tap, join, output stream,
connection queue.
See `Docs/TCPStreamer.md`.
### Packed Signals (Accumulate mode) ### Packed Signals (Accumulate mode)
When a signal has `NumberOfElements > 1` and `SamplingRate` is configured: When a signal has `NumberOfElements > 1` and `SamplingRate` is configured:
@@ -413,13 +380,9 @@ Hub = {
} }
``` ```
A TCP source sets `Transport = "tcp"` and, for a `TCPStreamService`, `StreamName = "<stream>"`;
a comma list (`"a,b"`) takes several streams over one connection and creates one session per
stream (label `<Label>_<stream>`, id `<node>_<stream>`).
Sources added at runtime (`addSource`) get generated ids `s1, s2, …`; Sources added at runtime (`addSource`) get generated ids `s1, s2, …`;
`saveSources` persists them to `SourcesFile` (JSON array of `saveSources` persists them to `SourcesFile` (JSON array of
`{label, addr, multicastGroup?, dataPort?, transport?, streamName?}`), reloaded at start-up. `{label, addr, multicastGroup?, dataPort?}`), reloaded at start-up.
`AllowedOrigins` is a comma/space-separated allowlist of `scheme://host[:port]` `AllowedOrigins` is a comma/space-separated allowlist of `scheme://host[:port]`
values accepted in the WebSocket `Origin` header (max 8 entries, 128 chars values accepted in the WebSocket `Origin` header (max 8 entries, 128 chars
-2
View File
@@ -84,8 +84,6 @@ Two independent data paths:
1. **Streaming path**: `UDPStreamer` DataSource serialises signals each RT cycle to UDPS binary packets (UDP 44500, unicast/multicast) → `StreamHub` (`Source/Applications/StreamHub/`, headless C++ app: ring buffers, LTTB decimation, trigger FSM) → WebSocket 8090 → browser (`Client/udpstreamer`, Go), native ImGui client (`Client/streamhub`), or native Qt client (`Client/streamhub-qt`). 1. **Streaming path**: `UDPStreamer` DataSource serialises signals each RT cycle to UDPS binary packets (UDP 44500, unicast/multicast) → `StreamHub` (`Source/Applications/StreamHub/`, headless C++ app: ring buffers, LTTB decimation, trigger FSM) → WebSocket 8090 → browser (`Client/udpstreamer`, Go), native ImGui client (`Client/streamhub`), or native Qt client (`Client/streamhub-qt`).
2. **Debug path**: `DebugService` patches the `ClassRegistryDatabase` at `Initialise()` so subsequent `ConfigureApplication()` instantiates `DebugBrokerWrapper<T>` around all `MemoryMap*Broker` types — no application changes. RT hot path goes through `DebugServiceI` (abstract singleton in `DebugServiceI.h`) for forcing/tracing/breakpoints. Exposes TCP 8080 (text commands), UDP 8081 (trace telemetry), works with `TcpLogger` on 8082. Web UI: `Client/debugger` (Go). 2. **Debug path**: `DebugService` patches the `ClassRegistryDatabase` at `Initialise()` so subsequent `ConfigureApplication()` instantiates `DebugBrokerWrapper<T>` around all `MemoryMap*Broker` types — no application changes. RT hot path goes through `DebugServiceI` (abstract singleton in `DebugServiceI.h`) for forcing/tracing/breakpoints. Exposes TCP 8080 (text commands), UDP 8081 (trace telemetry), works with `TcpLogger` on 8082. Web UI: `Client/debugger` (Go).
**TCP variant**: `TCPStreamer` / `TCPStreamerClient` (`Source/Components/DataSources/TCP*`) subclass `UDPStreamer` / `UDPStreamerClient` and swap the transport for `TCPSServer` / `TCPSClient` (`Source/Components/Interfaces/UDPStream/`) through the `UDPSServerI` / `UDPSClientI` seam in `UDPSTransportI.h`. Same UDPS framing (`totalFragments` always 1), per-client bounded queues so a slow client only drops its own frames. `TCPStreamService` (`Source/Components/Interfaces/TCPStreamService/`) is an optional single in-process server (acceptor + worker threads) shared by many `TCPStreamer`s via `Service=`/`StreamName=`; clients pick a stream with a SUBSCRIBE frame (UDPS type 5). A SUBSCRIBE may list several streams (`"a,b"`) to multiplex them over one socket (frames tagged with the stream index in `fragmentIdx`), and `TCPStreamJoin` publishes a merged stream (sample-and-hold of several streams paced by a master). StreamHub sources accept `Transport="tcp"` + `StreamName` (a comma list creates one session per stream). See `Docs/TCPStreamer.md`.
**Shared wire format**: `Common/UDP/UDPSProtocol.h` defines the UDPS binary protocol (17-byte packed header, 136-byte signal descriptors, little-endian). It is deliberately MARTe2-free so it's shared by C++ producers (`UDPStreamer`, `DebugService`), the C++ consumer (`Source/Components/Interfaces/UDPStream/UDPSClient`), the Go decoder (`Common/Client/go/udpsprotocol`), and the standalone C client (`Common/Client/c`, which redeclares the constants rather than including this header, so it stays MARTe-free). Changes to the protocol must be mirrored across all of these, plus the JS client parsers. **Shared wire format**: `Common/UDP/UDPSProtocol.h` defines the UDPS binary protocol (17-byte packed header, 136-byte signal descriptors, little-endian). It is deliberately MARTe2-free so it's shared by C++ producers (`UDPStreamer`, `DebugService`), the C++ consumer (`Source/Components/Interfaces/UDPStream/UDPSClient`), the Go decoder (`Common/Client/go/udpsprotocol`), and the standalone C client (`Common/Client/c`, which redeclares the constants rather than including this header, so it stays MARTe-free). Changes to the protocol must be mirrored across all of these, plus the JS client parsers.
**StreamHub WebSocket protocol**: JSON text frames for commands/events, binary frames for data pushes — spec in `ARCHITECTURE.md` §6. The Go hub (`Client/udpstreamer`) and C++ StreamHub implement the identical protocol; both clients (browser JS and ImGui) must stay compatible with both. **StreamHub WebSocket protocol**: JSON text frames for commands/events, binary frames for data pushes — spec in `ARCHITECTURE.md` §6. The Go hub (`Client/udpstreamer`) and C++ StreamHub implement the identical protocol; both clients (browser JS and ImGui) must stay compatible with both.
+2
View File
@@ -0,0 +1,2 @@
build/
compile_commands.json
+132
View File
@@ -0,0 +1,132 @@
cmake_minimum_required(VERSION 3.16)
project(UDPScope CXX C)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_C_STANDARD 99)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
option(UDPSCOPE_BUILD_TESTS "Build the unit tests" ON)
set(STREAMHUB_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../streamhub)
set(CCLIENT_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../Common/Client/c)
# ── The standalone C UDPS client, compiled in directly ────────────────────────
# Building it here rather than shelling out to its own Makefile keeps this a
# single cmake --build away from a working binary.
add_library(udpsclient STATIC ${CCLIENT_DIR}/udps_client.c)
target_include_directories(udpsclient PUBLIC ${CCLIENT_DIR})
target_compile_options(udpsclient PRIVATE -Wall -Wextra -Wpedantic)
# ── System packages ───────────────────────────────────────────────────────────
find_package(OpenGL REQUIRED)
find_package(SDL2 QUIET CONFIG)
if(NOT SDL2_FOUND)
find_package(PkgConfig REQUIRED)
pkg_check_modules(SDL2 REQUIRED sdl2)
add_library(SDL2::SDL2 INTERFACE IMPORTED)
target_include_directories(SDL2::SDL2 INTERFACE ${SDL2_INCLUDE_DIRS})
target_link_libraries(SDL2::SDL2 INTERFACE ${SDL2_LIBRARIES})
target_compile_options(SDL2::SDL2 INTERFACE ${SDL2_CFLAGS_OTHER})
endif()
# ── Dear ImGui + ImPlot ───────────────────────────────────────────────────────
include(FetchContent)
FetchContent_Declare(imgui
GIT_REPOSITORY https://github.com/ocornut/imgui.git
GIT_TAG v1.91.8
GIT_SHALLOW TRUE)
FetchContent_MakeAvailable(imgui)
FetchContent_Declare(implot
GIT_REPOSITORY https://github.com/epezent/implot.git
GIT_TAG v0.17
GIT_SHALLOW TRUE)
FetchContent_MakeAvailable(implot)
add_library(imgui_lib STATIC
${imgui_SOURCE_DIR}/imgui.cpp
${imgui_SOURCE_DIR}/imgui_draw.cpp
${imgui_SOURCE_DIR}/imgui_tables.cpp
${imgui_SOURCE_DIR}/imgui_widgets.cpp
${imgui_SOURCE_DIR}/backends/imgui_impl_sdl2.cpp
${imgui_SOURCE_DIR}/backends/imgui_impl_opengl3.cpp
${implot_SOURCE_DIR}/implot.cpp
${implot_SOURCE_DIR}/implot_items.cpp)
target_include_directories(imgui_lib PUBLIC
${imgui_SOURCE_DIR} ${imgui_SOURCE_DIR}/backends ${implot_SOURCE_DIR})
target_link_libraries(imgui_lib PUBLIC SDL2::SDL2 OpenGL::GL)
target_compile_options(imgui_lib PRIVATE -w)
# ── Bundled resources, borrowed read-only from the StreamHub client ───────────
set(RESOURCE_DIR ${STREAMHUB_DIR}/resources)
set(FONT_DIR ${RESOURCE_DIR}/fonts)
if(EXISTS ${FONT_DIR}/fa-solid-900.ttf AND EXISTS ${FONT_DIR}/IconsFontAwesome6.h)
set(HAVE_FONT_AWESOME TRUE)
message(STATUS "Font Awesome icons enabled (${FONT_DIR})")
else()
set(HAVE_FONT_AWESOME FALSE)
message(WARNING "Bundled Font Awesome missing — using ASCII icon fallbacks")
endif()
# Guarded: file(COPY) is a hard configure error on a missing source, which
# would defeat the fallback the block above just chose.
if(EXISTS ${FONT_DIR})
file(COPY ${FONT_DIR} DESTINATION ${CMAKE_BINARY_DIR}/resources)
endif()
# ── Core library: everything except main.cpp, so tests can link it ────────────
set(CORE_SOURCES
Decimate.cpp
PaneTree.cpp
TimeBase.cpp
FrameDecoder.cpp
)
add_library(udpscope_core STATIC ${CORE_SOURCES})
target_include_directories(udpscope_core PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}
${STREAMHUB_DIR}) # SignalBuffer.h, reused verbatim
target_link_libraries(udpscope_core PUBLIC udpsclient pthread)
target_compile_options(udpscope_core PRIVATE -Wall -Wextra -Wno-unused-parameter)
# ── Application ───────────────────────────────────────────────────────────────
set(APP_SOURCES
main.cpp
)
if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/main.cpp)
add_executable(UDPScope ${APP_SOURCES})
target_link_libraries(UDPScope PRIVATE udpscope_core imgui_lib SDL2::SDL2 OpenGL::GL)
target_compile_definitions(UDPScope PRIVATE APP_RESOURCE_DIR="${RESOURCE_DIR}")
if(HAVE_FONT_AWESOME)
target_include_directories(UDPScope PRIVATE ${FONT_DIR})
target_compile_definitions(UDPScope PRIVATE HAVE_FONT_AWESOME)
endif()
target_compile_options(UDPScope PRIVATE -Wall -Wextra -Wno-unused-parameter)
install(TARGETS UDPScope DESTINATION bin)
install(DIRECTORY ${FONT_DIR} DESTINATION share/udpscope)
endif()
# ── Tests ─────────────────────────────────────────────────────────────────────
if(UDPSCOPE_BUILD_TESTS)
FetchContent_Declare(googletest
GIT_REPOSITORY https://github.com/google/googletest.git
GIT_TAG v1.15.2
GIT_SHALLOW TRUE)
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(googletest)
file(GLOB TEST_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/tests/*.cpp)
add_executable(udpscope_tests ${TEST_SOURCES})
target_link_libraries(udpscope_tests PRIVATE udpscope_core GTest::gtest_main)
target_compile_options(udpscope_tests PRIVATE -Wall -Wextra -Wno-unused-parameter)
enable_testing()
include(GoogleTest)
gtest_discover_tests(udpscope_tests)
endif()
+47
View File
@@ -0,0 +1,47 @@
#include "Decimate.h"
#include <algorithm>
namespace udpscope {
void MinMaxDecimate(const double* t, const double* v, size_t n,
size_t maxPoints, Series& out) {
out.clear();
if (n == 0 || t == nullptr || v == nullptr) {
return;
}
if (n <= maxPoints || maxPoints < 4) {
out.t.assign(t, t + n);
out.v.assign(v, v + n);
return;
}
/* Two points per bucket, so the bucket count is half the budget. */
const size_t buckets = maxPoints / 2;
out.t.reserve(buckets * 2);
out.v.reserve(buckets * 2);
for (size_t b = 0; b < buckets; b++) {
const size_t begin = (n * b) / buckets;
size_t end = (n * (b + 1)) / buckets;
if (end <= begin) { end = begin + 1; }
if (end > n) { end = n; }
size_t lo = begin, hi = begin;
for (size_t i = begin + 1; i < end; i++) {
if (v[i] < v[lo]) { lo = i; }
if (v[i] > v[hi]) { hi = i; }
}
const size_t first = std::min(lo, hi);
const size_t second = std::max(lo, hi);
out.t.push_back(t[first]);
out.v.push_back(v[first]);
if (second != first) {
out.t.push_back(t[second]);
out.v.push_back(v[second]);
}
}
}
} /* namespace udpscope */
+25
View File
@@ -0,0 +1,25 @@
/**
* @file Decimate.h
* @brief Min/max envelope decimation for screen rendering.
*/
#pragma once
#include "Types.h"
namespace udpscope {
/**
* @brief Reduce n points to at most maxPoints by emitting each bucket's
* minimum and maximum, in time order.
*
* LTTB is deliberately not used. It selects representative points and will
* silently drop a one-sample glitch; on a scope that glitch is usually the
* thing being looked for. The emitted pair stays in time order rather than
* value order because callers binary-search the result by time.
*
* Input shorter than maxPoints is copied through unchanged.
*/
void MinMaxDecimate(const double* t, const double* v, size_t n,
size_t maxPoints, Series& out);
} /* namespace udpscope */
+192
View File
@@ -0,0 +1,192 @@
#include "FrameDecoder.h"
#include <cmath>
namespace udpscope {
/** Fallback cycle period before the first inter-packet gap is known. */
static constexpr double kDefaultDt = 1.0e-3;
/**
* How far the forward-chained prediction for an accumulated burst may sit from
* where arrival time says it should be before the chain is abandoned.
*
* A kernel draining a backlog of queued datagrams can legitimately put the
* prediction a few hundred milliseconds ahead of arrival, so the threshold has
* to be well clear of that. Anything larger is not delivery jitter: it is lost
* packets or a declared sampling rate that does not match the producer's real
* one, and both must resynchronise rather than accumulate forever. Same value
* and same reasoning as ClockOffset::kRecalibThresholdS.
*/
static constexpr double kBurstResyncThresholdS = 0.5;
void FrameDecoder::setSignals(const std::vector<SignalMeta>& signals) {
signals_ = signals;
state_.assign(signals_.size(), SigState{});
hrtFit_.reset();
}
void FrameDecoder::reset() {
state_.assign(signals_.size(), SigState{});
hrtFit_.reset();
}
void FrameDecoder::beginFrame(const FrameView& f) {
if (f.hrt != 0u) { hrtFit_.add(f.hrt, f.recvTime); }
}
bool FrameDecoder::packetBurst(uint32_t idx, uint32_t nElems, double wallNow,
std::vector<double>& tsOut) {
SigState& st = state_[idx];
if (!st.lastPacketValid || wallNow <= st.lastPacketWall) {
/* No previous arrival to span from, or time went backwards. Remember
* this one and drop the samples rather than store them at made-up
* spacing. */
st.lastPacketWall = wallNow;
st.lastPacketValid = true;
return false;
}
const double dt = (wallNow - st.lastPacketWall) / static_cast<double>(nElems);
tsOut.resize(nElems);
for (uint32_t e = 0; e < nElems; e++) {
tsOut[e] = st.lastPacketWall + static_cast<double>(e + 1u) * dt;
}
st.lastPacketWall = wallNow;
return true;
}
bool FrameDecoder::timestamps(const FrameView& f, uint32_t idx,
std::vector<double>& tsOut) {
tsOut.clear();
if (idx >= signals_.size() || idx >= f.numSignals || f.counts == nullptr) {
return false;
}
const SignalMeta& d = signals_[idx];
const uint32_t nElems = f.counts[idx];
if (nElems == 0u) { return false; }
const double wallNow = f.recvTime;
SigState& st = state_[idx];
const bool hasTimeSig = d.hasTimeSignal(f.numSignals);
const uint32_t tIdx = hasTimeSig ? d.timeSignalIdx : 0u;
const double tScale = hasTimeSig
? TimeSignalScale(signals_[tIdx].typeCode)
: 1.0e-6;
/* Rule 1: one stamp per element, straight from the time signal. */
if (d.timeMode == kTimeFullArray && hasTimeSig &&
f.counts[tIdx] >= nElems && f.values[tIdx] != nullptr) {
const double* tv = f.values[tIdx];
const double t0 = tv[0] * tScale;
(void) st.offset.map(t0, wallNow);
const double base = st.offset.offset();
tsOut.resize(nElems);
for (uint32_t e = 0; e < nElems; e++) {
tsOut[e] = base + tv[e] * tScale;
}
return true;
}
/* Rule 2: anchor from the time signal, spread by the sampling rate. */
if ((d.timeMode == kTimeFirstSample || d.timeMode == kTimeLastSample) &&
hasTimeSig && f.counts[tIdx] >= 1u && f.values[tIdx] != nullptr) {
const double anchor = st.offset.map(f.values[tIdx][0] * tScale, wallNow);
const double dt = (d.samplingRate > 0.0) ? (1.0 / d.samplingRate) : 0.0;
tsOut.resize(nElems);
for (uint32_t e = 0; e < nElems; e++) {
tsOut[e] = (d.timeMode == kTimeFirstSample)
? (anchor + static_cast<double>(e) * dt)
: (anchor - static_cast<double>(nElems - 1u - e) * dt);
}
return true;
}
/* Rule 3: accumulated scalar, based on declared sampling rate or hrt.
*
* When samplingRate is declared the inter-element step is exact and we
* anchor from the end of the previous burst rather than from arrival time
* or hrt. This makes the output immune to arrival jitter: even when the
* kernel delivers two packets microseconds apart each burst starts exactly
* one sample period after the previous burst ended.
*
* When samplingRate is absent we must derive dt from the hrt gap, which
* requires the HrtRateFit to be ready. Until then we fall back to
* packetBurst (arrival-time spanning), which is accurate during the normal
* pre-burst delivery phase that precedes the fit becoming ready. */
if (d.numElements() == 1u && nElems > 1u) {
const double dt = (d.samplingRate > 0.0)
? (1.0 / d.samplingRate)
: 0.0;
if (d.samplingRate > 0.0) {
/* Where arrival time says this burst begins: its last element was
* acquired just before the packet landed. */
const double arrivalAnchor =
wallNow - static_cast<double>(nElems - 1u) * dt;
/* Chaining from the end of the previous burst is immune to arrival
* jitter — a kernel draining several queued datagrams microseconds
* apart still yields contiguous timestamps. But a pure chain is
* blind: one lost datagram, or a declared rate that does not match
* the producer's real one, displaces every later sample and never
* recovers. So the chain is a PREDICTION, checked each packet
* against arrival and abandoned when the two disagree by more than
* a delivery backlog can explain. That bounds the error instead of
* letting it accumulate. */
double base = arrivalAnchor;
if (st.lastEmittedValid) {
const double predicted = st.lastEmittedEnd + dt;
if (std::fabs(predicted - arrivalAnchor) <= kBurstResyncThresholdS) {
base = predicted;
}
}
tsOut.resize(nElems);
for (uint32_t e = 0; e < nElems; e++) {
tsOut[e] = base + static_cast<double>(e) * dt;
}
st.lastEmittedEnd = tsOut[nElems - 1u];
st.lastEmittedValid = true;
return true;
}
/* No declared rate: need hrt-derived dt. */
if (!hrtFit_.ready()) {
return packetBurst(idx, nElems, wallNow, tsOut);
}
const double hrtSec = hrtFit_.toSeconds(f.hrt);
const double base = st.offset.map(hrtSec, wallNow);
double hrtDt;
if (st.lastAccValid && st.prevAccCount > 0u && hrtSec > st.lastAccHrtSec) {
/* The flushes carry contiguous RT cycles, so the gap divided by the
* previous packet's sample count is exactly one cycle period. */
hrtDt = (hrtSec - st.lastAccHrtSec) /
static_cast<double>(st.prevAccCount);
} else {
hrtDt = kDefaultDt;
}
tsOut.resize(nElems);
for (uint32_t e = 0; e < nElems; e++) {
tsOut[e] = base + static_cast<double>(e) * hrtDt;
}
st.lastAccHrtSec = hrtSec;
st.lastAccValid = true;
st.prevAccCount = nElems;
return true;
}
/* Rule 4: PACKET burst with no time reference at all. */
if (nElems > 1u) {
return packetBurst(idx, nElems, wallNow, tsOut);
}
/* Rule 5: plain scalar. */
tsOut.assign(1, wallNow);
return true;
}
} /* namespace udpscope */
+71
View File
@@ -0,0 +1,71 @@
/**
* @file FrameDecoder.h
* @brief Per-element timestamp reconstruction for UDPS frames.
*
* The C client's udps_frame_element_time() is explicitly an arrival-anchored
* estimate. It is not sufficient: the kernel frequently delivers several queued
* datagrams in one burst, so two packets are processed microseconds apart even
* though each represents ~10 ms of signal, and arrival-time interpolation then
* crams a packet's samples into that tiny gap — the trace renders as a sawtooth.
* Source/Applications/StreamHub/UDPSourceSession.cpp documents this failure and
* solves it; these are the same rules, computed from udps_frame_t's own fields
* so the scope and StreamHub agree on the same stream.
*/
#pragma once
#include "TimeBase.h"
#include "Types.h"
#include <vector>
namespace udpscope {
class FrameDecoder {
public:
/** Installs the signal table. Clears all per-signal timing history. */
void setSignals(const std::vector<SignalMeta>& signals);
const std::vector<SignalMeta>& signals() const { return signals_; }
/** Call once per frame, before any timestamps() call for that frame. */
void beginFrame(const FrameView& f);
/**
* @brief Timestamps for every value of signal @p idx in this frame.
* @return false when the signal produced nothing usable — an empty slot, or
* the first PACKET burst after connect, which has no previous
* arrival to span from and would otherwise poison the ring with
* wrongly spaced timestamps.
*/
bool timestamps(const FrameView& f, uint32_t idx, std::vector<double>& tsOut);
/** Forgets all timing history; call on reconnect. */
void reset();
private:
bool packetBurst(uint32_t idx, uint32_t nElems, double wallNow,
std::vector<double>& tsOut);
struct SigState {
ClockOffset offset;
double lastPacketWall = 0.0;
bool lastPacketValid = false;
double lastAccHrtSec = 0.0;
bool lastAccValid = false;
uint32_t prevAccCount = 0;
/** For accumulated scalars with a declared sampling rate: end timestamp
* of the most recently emitted burst. The next burst is PREDICTED to
* start one sample period after it — immune to arrival-time jitter —
* but the prediction is discarded when arrival time disagrees with it
* by more than a delivery backlog can explain, so packet loss cannot
* displace the trace permanently. */
double lastEmittedEnd = 0.0;
bool lastEmittedValid = false;
};
std::vector<SignalMeta> signals_;
std::vector<SigState> state_;
HrtRateFit hrtFit_;
};
} /* namespace udpscope */
+157
View File
@@ -0,0 +1,157 @@
#include "PaneTree.h"
#include <algorithm>
#include <cmath>
namespace udpscope {
PaneTree::PaneTree() : root_(new PaneNode()) {}
void PaneTree::setRoot(std::unique_ptr<PaneNode> node) {
if (node) { root_ = std::move(node); }
}
double PaneTree::clampRatio(double ratio, double extent) {
if (extent <= 2.0 * kMinPaneSize) {
return 0.5; /* Too small to honour the minimum on both sides. */
}
const double lo = kMinPaneSize / extent;
return std::min(std::max(ratio, lo), 1.0 - lo);
}
void PaneTree::layoutNode(PaneNode* node, const Rect& r,
std::vector<Placed>& leaves,
std::vector<Splitter>& splitters) {
if (node == nullptr) { return; }
if (node->leaf) {
leaves.push_back(Placed{node, r});
return;
}
if (node->orient == Orient::Columns) {
const double ratio = clampRatio(node->ratio, r.w);
const double wA = r.w * ratio;
layoutNode(node->a.get(), Rect{r.x, r.y, wA, r.h}, leaves, splitters);
layoutNode(node->b.get(), Rect{r.x + wA, r.y, r.w - wA, r.h}, leaves, splitters);
splitters.push_back(Splitter{
node,
Rect{r.x + wA - kSplitterGrab * 0.5, r.y, kSplitterGrab, r.h},
Orient::Columns});
} else {
const double ratio = clampRatio(node->ratio, r.h);
const double hA = r.h * ratio;
layoutNode(node->a.get(), Rect{r.x, r.y, r.w, hA}, leaves, splitters);
layoutNode(node->b.get(), Rect{r.x, r.y + hA, r.w, r.h - hA}, leaves, splitters);
splitters.push_back(Splitter{
node,
Rect{r.x, r.y + hA - kSplitterGrab * 0.5, r.w, kSplitterGrab},
Orient::Rows});
}
}
void PaneTree::layout(const Rect& area,
std::vector<Placed>& leaves,
std::vector<Splitter>& splitters) const {
leaves.clear();
splitters.clear();
layoutNode(root_.get(), area, leaves, splitters);
}
void PaneTree::splitLeaf(PaneNode* leaf, Orient orient) {
if (leaf == nullptr || !leaf->leaf) { return; }
/* Move the existing content into a new first child; the second is empty. */
std::unique_ptr<PaneNode> first(new PaneNode());
first->signals = std::move(leaf->signals);
first->profilePane = leaf->profilePane;
std::unique_ptr<PaneNode> second(new PaneNode());
leaf->leaf = false;
leaf->orient = orient;
leaf->ratio = 0.5;
leaf->signals.clear();
leaf->a = std::move(first);
leaf->b = std::move(second);
}
PaneNode* PaneTree::findParent(PaneNode* node, const PaneNode* child) {
if (node == nullptr || node->leaf) { return nullptr; }
if (node->a.get() == child || node->b.get() == child) { return node; }
if (PaneNode* p = findParent(node->a.get(), child)) { return p; }
return findParent(node->b.get(), child);
}
void PaneTree::closeLeaf(PaneNode* leaf) {
if (leaf == nullptr || !leaf->leaf) { return; }
PaneNode* parent = findParent(root_.get(), leaf);
if (parent == nullptr) {
return; /* The root is the only leaf; a scope with no pane is useless. */
}
std::unique_ptr<PaneNode> survivor =
(parent->a.get() == leaf) ? std::move(parent->b) : std::move(parent->a);
/* Collapse the parent into the survivor in place, so the parent pointer
* held by any caller stays valid. */
parent->leaf = survivor->leaf;
parent->signals = std::move(survivor->signals);
parent->profilePane = survivor->profilePane;
parent->orient = survivor->orient;
parent->ratio = survivor->ratio;
parent->a = std::move(survivor->a);
parent->b = std::move(survivor->b);
}
void PaneTree::setRatio(PaneNode* split, double ratio) {
if (split != nullptr && !split->leaf) {
split->ratio = std::min(std::max(ratio, 0.0), 1.0);
}
}
size_t PaneTree::countLeaves(const PaneNode* node) {
if (node == nullptr) { return 0; }
if (node->leaf) { return 1; }
return countLeaves(node->a.get()) + countLeaves(node->b.get());
}
size_t PaneTree::leafCount() const { return countLeaves(root_.get()); }
const PaneTree::Splitter* PaneTree::hitTestSplitter(
const std::vector<Splitter>& splitters, double px, double py) const {
for (const Splitter& s : splitters) {
if (s.rect.contains(px, py)) { return &s; }
}
return nullptr;
}
Handle PaneTree::hitTestHandle(const Rect& pane, double px, double py) {
if (!pane.contains(px, py)) { return Handle::None; }
const double relX = px - pane.x;
const double relY = py - pane.y;
const double midY = pane.h * 0.5;
const double midX = pane.w * 0.5;
const double half = kHandleSize * 0.5;
/* Close sits in the top-right corner and wins over the edge handles. */
if (relX >= pane.w - kHandleSize && relY <= kHandleSize) {
return Handle::Close;
}
if (relX <= kHandleSize && std::abs(relY - midY) <= half * 3.0) {
return Handle::Left;
}
if (relX >= pane.w - kHandleSize && std::abs(relY - midY) <= half * 3.0) {
return Handle::Right;
}
if (relY <= kHandleSize && std::abs(relX - midX) <= half * 3.0) {
return Handle::Top;
}
if (relY >= pane.h - kHandleSize && std::abs(relX - midX) <= half * 3.0) {
return Handle::Bottom;
}
return Handle::None;
}
} /* namespace udpscope */
+116
View File
@@ -0,0 +1,116 @@
/**
* @file PaneTree.h
* @brief Binary-space-partition layout of the plot area.
*
* Framework-free: no ImGui, no UDPS. The geometry and the hit-testing are the
* fiddly part of the pane UI and are unit-tested without a window.
*/
#pragma once
#include "Types.h"
#include <memory>
#include <string>
#include <vector>
namespace udpscope {
/** Direction a node splits its rectangle in. */
enum class Orient { Columns, Rows };
/** Vertical scaling strategy for one trace. */
enum class VMode { Auto, Range, Manual };
struct VScale {
VMode mode = VMode::Auto;
double div = 1.0; /**< Units per division, Manual only. */
double offset = 0.0; /**< Centre value, Manual only. */
};
/** One signal drawn in one pane. Signals are named, never indexed. */
struct Assignment {
std::string signalName;
Color color;
float lineWidth = 1.5f;
VScale vs;
};
/** Smallest a pane may be squeezed to, in pixels. */
constexpr double kMinPaneSize = 80.0;
/** Thickness of the splitter drag zone and of the inset handles, in pixels. */
constexpr double kSplitterGrab = 6.0;
constexpr double kHandleSize = 18.0;
/** What the pointer is over inside a pane. */
enum class Handle { None, Left, Right, Top, Bottom, Close };
struct PaneNode {
bool leaf = true;
/* leaf only */
std::vector<Assignment> signals;
bool profilePane = false; /**< Holds vector signals, not time series. */
/* split only */
Orient orient = Orient::Columns;
double ratio = 0.5; /**< First child's share of the parent. */
std::unique_ptr<PaneNode> a, b;
};
class PaneTree {
public:
struct Placed { PaneNode* leaf; Rect rect; };
struct Splitter { PaneNode* node; Rect rect; Orient orient; };
PaneTree();
PaneNode* root() { return root_.get(); }
const PaneNode* root() const { return root_.get(); }
/** Replaces the whole tree, e.g. when loading a session. */
void setRoot(std::unique_ptr<PaneNode> node);
/**
* @brief Walk the tree, producing every leaf's rectangle and every split's
* drag zone.
*/
void layout(const Rect& area,
std::vector<Placed>& leaves,
std::vector<Splitter>& splitters) const;
/** Turn a leaf into a split; the original content stays in the first child. */
void splitLeaf(PaneNode* leaf, Orient orient);
/** Replace the leaf's parent with its sibling. No-op on the last leaf. */
void closeLeaf(PaneNode* leaf);
void setRatio(PaneNode* split, double ratio);
size_t leafCount() const;
/** @return the splitter under the point, or nullptr. */
const Splitter* hitTestSplitter(const std::vector<Splitter>& splitters,
double px, double py) const;
/**
* @brief Which inset handle of @p pane the point is over.
*
* Handles sit inside the pane so they never overlap the splitter drag zone,
* and every pane has all four regardless of whether it touches a window
* edge — a pane in the middle of a 3x3 touches none.
*/
static Handle hitTestHandle(const Rect& pane, double px, double py);
private:
static void layoutNode(PaneNode* node, const Rect& r,
std::vector<Placed>& leaves,
std::vector<Splitter>& splitters);
static size_t countLeaves(const PaneNode* node);
static PaneNode* findParent(PaneNode* node, const PaneNode* child);
static double clampRatio(double ratio, double extent);
std::unique_ptr<PaneNode> root_;
};
} /* namespace udpscope */
+74
View File
@@ -0,0 +1,74 @@
#include "TimeBase.h"
#include <cmath>
namespace udpscope {
/* UDPS_T_UINT64 == 6 in Common/UDP/UDPSProtocol.h. Spelled numerically so this
* translation unit stays free of the C client header. */
static constexpr uint8_t kTypeUint64 = 6u;
double TimeSignalScale(uint8_t typeCode) {
return (typeCode == kTypeUint64) ? 1.0e-9 : 1.0e-6;
}
double ClockOffset::map(double producerSec, double wallSec) {
/* Symmetric on purpose. Delivery jitter of a few tens of ms either side of
* the prediction must not reset the offset or the whole trace wobbles, but a
* producer clock that steps in EITHER direction has to be picked up: a
* restart leaves the prediction behind the wall clock, an NTP correction on
* the producer's host leaves it ahead. A one-sided test silently never fires
* for the second case and the trace sits in the future for the whole run. */
if (!valid_ || std::fabs(wallSec - (offset_ + producerSec)) > kRecalibThresholdS) {
offset_ = wallSec - producerSec;
valid_ = true;
}
return offset_ + producerSec;
}
void HrtRateFit::reset() {
samples_.clear();
n_ = 0;
rate_ = 0.0;
}
void HrtRateFit::add(uint64_t hrt, double wallSec) {
samples_.push_back(Sample{static_cast<double>(hrt), wallSec});
if (samples_.size() > kWindow) { samples_.pop_front(); }
n_++;
if (n_ >= kMinSamples) { refit(); }
}
void HrtRateFit::refit() {
const size_t n = samples_.size();
if (n < 2) { return; }
/* Least squares slope of hrt against wall time. Both are subtracted from
* their first value first: raw hrt counts and epoch seconds are large
* enough that the naive sums lose precision. */
const double h0 = samples_.front().hrt;
const double w0 = samples_.front().wall;
double sw = 0.0, sh = 0.0, sww = 0.0, swh = 0.0;
for (const Sample& s : samples_) {
const double w = s.wall - w0;
const double h = s.hrt - h0;
sw += w;
sh += h;
sww += w * w;
swh += w * h;
}
const double dn = static_cast<double>(n);
const double denom = dn * sww - sw * sw;
if (std::fabs(denom) < 1e-12) { return; }
const double slope = (dn * swh - sw * sh) / denom;
if (slope > 0.0 && std::isfinite(slope)) { rate_ = slope; }
}
double HrtRateFit::toSeconds(uint64_t hrt) const {
if (rate_ <= 0.0) { return 0.0; }
return static_cast<double>(hrt) / rate_;
}
} /* namespace udpscope */
+87
View File
@@ -0,0 +1,87 @@
/**
* @file TimeBase.h
* @brief Producer-clock to wall-clock reconstruction.
*
* Framework-free. A UDPS stream's accurate timestamps come from a producer
* clock — either a declared time signal or the packet's embedded hrt — and both
* need mapping onto the client's wall clock before they can be plotted.
*/
#pragma once
#include <cstddef>
#include <cstdint>
#include <deque>
namespace udpscope {
/**
* @brief Seconds per count of a time signal, from its type code.
*
* The protocol carries uint64 time signals in nanoseconds and everything else
* in microseconds; this mirrors UDPSourceSession so the two agree on a stream.
*/
double TimeSignalScale(uint8_t typeCode);
/**
* @brief A latched producer-to-wall offset.
*
* Established from the first sample and then held, so network jitter does not
* wobble the trace. Only a drift beyond kRecalibThresholdS — a producer restart
* or re-phase, not delivery noise — forces a new calibration.
*/
class ClockOffset {
public:
static constexpr double kRecalibThresholdS = 0.5;
/** @return producerSec mapped onto wall clock. */
double map(double producerSec, double wallSec);
bool valid() const { return valid_; }
void reset() { valid_ = false; offset_ = 0.0; }
double offset() const { return offset_; }
private:
double offset_ = 0.0;
bool valid_ = false;
};
/**
* @brief Recovers the producer's hrt tick rate by least squares against arrival
* time.
*
* The protocol does not carry the tick rate, and StreamHub's approach of using
* the local MARTe HighResolutionTimer frequency is only valid when the client
* runs on the producer's host. A remote bench scope cannot assume that, so the
* rate is measured: hrt against recv_time is a straight line whose slope is
* ticks per second.
*/
class HrtRateFit {
public:
static constexpr size_t kMinSamples = 32;
static constexpr size_t kWindow = 256;
void add(uint64_t hrt, double wallSec);
bool ready() const { return n_ >= kMinSamples && rate_ > 0.0; }
double ticksPerSecond() const { return rate_; }
/**
* @brief Converts a tick count to seconds on the PRODUCER's own epoch.
*
* The fit recovers the slope only and discards the intercept, so this is
* `hrt / ticksPerSecond()` — not a wall-clock time. A producer's hrt counts
* from its own boot, not from the Unix epoch. Pass the result to
* ClockOffset::map() to land it on the wall clock; latching that arbitrary
* epoch difference is precisely what ClockOffset is for.
*/
double toSeconds(uint64_t hrt) const;
void reset();
private:
void refit();
struct Sample { double hrt; double wall; };
std::deque<Sample> samples_;
size_t n_ = 0;
double rate_ = 0.0;
};
} /* namespace udpscope */
+103
View File
@@ -0,0 +1,103 @@
/**
* @file Types.h
* @brief Plain data shared across UDPScope modules. No logic, no dependencies.
*/
#pragma once
#include <cstddef>
#include <cstdint>
#include <string>
#include <vector>
namespace udpscope {
/** A time series as two parallel arrays, which is what ImPlot wants. */
struct Series {
std::vector<double> t;
std::vector<double> v;
void clear() { t.clear(); v.clear(); }
size_t size() const { return t.size(); }
bool empty() const { return t.empty(); }
};
/** RGBA in 0..1. Framework-free so PaneTree needs no ImGui. */
struct Color {
float r = 1.f, g = 1.f, b = 1.f, a = 1.f;
};
/** Screen rectangle in pixels. */
struct Rect {
double x = 0.0, y = 0.0, w = 0.0, h = 0.0;
bool contains(double px, double py) const {
return px >= x && px < (x + w) && py >= y && py < (y + h);
}
};
/* Protocol constants, spelled out rather than included, so the framework-free
* modules stay independent of udps_client.h. They mirror Common/UDP/UDPSProtocol.h. */
constexpr uint8_t kTimePacket = 0;
constexpr uint8_t kTimeFullArray = 1;
constexpr uint8_t kTimeFirstSample = 2;
constexpr uint8_t kTimeLastSample = 3;
constexpr uint32_t kNoTimeSignal = 0xFFFFFFFFu;
/** Framework-free mirror of udps_signal_t, plus UI state. */
struct SignalMeta {
std::string name;
uint8_t typeCode = 255;
uint8_t quantType = 0;
uint32_t numRows = 1;
uint32_t numCols = 1;
double rangeMin = 0.0;
double rangeMax = 0.0;
uint8_t timeMode = kTimePacket;
double samplingRate = 0.0;
uint32_t timeSignalIdx = kNoTimeSignal;
std::string unit;
/** User override: treat an ambiguous PACKET array as a profile, not a burst. */
bool profileOverride = false;
uint32_t numElements() const {
const uint64_t n = static_cast<uint64_t>(numRows ? numRows : 1u) *
static_cast<uint64_t>(numCols ? numCols : 1u);
return n == 0u ? 1u : static_cast<uint32_t>(n);
}
bool hasTimeSignal(uint32_t numSignals) const {
return timeSignalIdx != kNoTimeSignal && timeSignalIdx < numSignals;
}
/**
* @brief True when this array should be plotted against element index
* rather than unrolled onto the time axis.
*
* Only PACKET arrays are ambiguous: the producer stamped the whole datagram
* with one time, which is what a genuine vector looks like and also what a
* burst carrying no time metadata looks like. Default is burst, matching
* UDPSourceSession, with this flag as the user's override.
*/
bool isVectorProfile() const {
return profileOverride && numElements() > 1u && timeMode == kTimePacket;
}
};
/**
* @brief Non-owning mirror of udps_frame_t.
*
* Kept separate from the C struct so FrameDecoder can be tested with plain
* arrays and no socket. Points at memory owned by the caller.
*/
struct FrameView {
uint32_t counter = 0;
uint64_t hrt = 0;
double recvTime = 0.0;
uint32_t numSamples = 1;
uint32_t numSignals = 0;
const double* const* values = nullptr; /**< values[i][0..counts[i]) */
const uint32_t* counts = nullptr;
};
} /* namespace udpscope */
+90
View File
@@ -0,0 +1,90 @@
#include "Decimate.h"
#include <gtest/gtest.h>
#include <algorithm>
#include <vector>
using namespace udpscope;
TEST(MinMaxDecimate, PassesShortInputThroughUnchanged) {
const std::vector<double> t{0.0, 1.0, 2.0};
const std::vector<double> v{5.0, 6.0, 7.0};
Series out;
MinMaxDecimate(t.data(), v.data(), t.size(), 100, out);
EXPECT_EQ(out.t, t);
EXPECT_EQ(out.v, v);
}
// The whole reason for preferring min/max over LTTB: a single-sample spike is
// usually the thing the user is looking for, and it must survive decimation.
TEST(MinMaxDecimate, PreservesAnIsolatedSpike) {
std::vector<double> t(1000), v(1000, 0.0);
for (size_t i = 0; i < t.size(); i++) { t[i] = static_cast<double>(i); }
v[437] = 42.0;
Series out;
MinMaxDecimate(t.data(), v.data(), t.size(), 50, out);
ASSERT_FALSE(out.v.empty());
EXPECT_EQ(*std::max_element(out.v.begin(), out.v.end()), 42.0);
}
TEST(MinMaxDecimate, PreservesTheExtremesOfEveryBucket) {
std::vector<double> t(100), v(100);
for (size_t i = 0; i < t.size(); i++) {
t[i] = static_cast<double>(i);
v[i] = (i % 10 == 3) ? -9.0 : ((i % 10 == 7) ? 9.0 : 0.0);
}
Series out;
MinMaxDecimate(t.data(), v.data(), t.size(), 20, out);
EXPECT_EQ(*std::min_element(out.v.begin(), out.v.end()), -9.0);
EXPECT_EQ(*std::max_element(out.v.begin(), out.v.end()), 9.0);
}
// A ring whose timestamps are not monotonic breaks any later binary search by
// time, so the pair emitted per bucket must be ordered by time, not by value.
TEST(MinMaxDecimate, EmitsPointsInTimeOrder) {
// Two buckets of four. In the first the minimum comes before the maximum,
// in the second the order is reversed. An implementation that emitted
// (min, max) by value rather than by time passes on bucket 0 and fails on
// bucket 1, so this data exercises the swap that a monotonically growing
// ramp never triggers.
const double st[8] = {0, 1, 2, 3, 4, 5, 6, 7};
const double sv[8] = {-5, 0, 0, 9, 9, 0, 0, -5};
Series pair;
MinMaxDecimate(st, sv, 8, 4, pair);
ASSERT_EQ(pair.size(), 4u);
const double wantT[4] = {0, 3, 4, 7};
const double wantV[4] = {-5, 9, 9, -5};
for (size_t i = 0; i < 4; i++) {
EXPECT_EQ(pair.t[i], wantT[i]) << "time at " << i;
EXPECT_EQ(pair.v[i], wantV[i]) << "value at " << i;
}
std::vector<double> t(400), v(400);
for (size_t i = 0; i < t.size(); i++) {
t[i] = static_cast<double>(i);
v[i] = (i % 2 == 0) ? -static_cast<double>(i) : static_cast<double>(i);
}
Series out;
MinMaxDecimate(t.data(), v.data(), t.size(), 40, out);
ASSERT_GT(out.t.size(), 1u);
for (size_t i = 1; i < out.t.size(); i++) {
EXPECT_LE(out.t[i - 1], out.t[i]) << "at index " << i;
}
}
TEST(MinMaxDecimate, HandlesEmptyInput) {
Series out;
out.t.push_back(1.0); // must be cleared
MinMaxDecimate(nullptr, nullptr, 0, 10, out);
EXPECT_TRUE(out.t.empty());
EXPECT_TRUE(out.v.empty());
}
+315
View File
@@ -0,0 +1,315 @@
#include "FrameDecoder.h"
#include <gtest/gtest.h>
#include <vector>
using namespace udpscope;
namespace {
/** Builds a FrameView over vectors the test owns. */
struct FrameBuilder {
std::vector<std::vector<double>> storage;
std::vector<const double*> ptrs;
std::vector<uint32_t> counts;
FrameView view;
void addSignal(std::vector<double> vals) {
storage.push_back(std::move(vals));
}
const FrameView& build(uint64_t hrt, double recvTime, uint32_t numSamples = 1) {
ptrs.clear();
counts.clear();
for (const auto& s : storage) {
ptrs.push_back(s.data());
counts.push_back(static_cast<uint32_t>(s.size()));
}
view.hrt = hrt;
view.recvTime = recvTime;
view.numSamples = numSamples;
view.numSignals = static_cast<uint32_t>(storage.size());
view.values = ptrs.data();
view.counts = counts.data();
return view;
}
};
SignalMeta burst(const char* name, uint8_t timeMode, double rate,
uint32_t elems, uint32_t timeIdx) {
SignalMeta m;
m.name = name;
m.typeCode = 8; /* float32 */
m.numRows = elems;
m.numCols = 1;
m.timeMode = timeMode;
m.samplingRate = rate;
m.timeSignalIdx = timeIdx;
return m;
}
SignalMeta timeSignal(const char* name, uint32_t elems) {
SignalMeta m;
m.name = name;
m.typeCode = 6; /* uint64 -> nanoseconds */
m.numRows = elems;
m.numCols = 1;
return m;
}
} /* namespace */
TEST(FrameDecoder, FullArrayTakesOneStampPerElementFromTheTimeSignal) {
FrameDecoder dec;
dec.setSignals({burst("Sine", kTimeFullArray, 1000.0, 4, 1),
timeSignal("Time", 4)});
FrameBuilder fb;
fb.addSignal({1.0, 2.0, 3.0, 4.0});
/* Nanoseconds: 5.000, 5.001, 5.002, 5.003 s of producer time. */
fb.addSignal({5.0e9, 5.001e9, 5.002e9, 5.003e9});
const FrameView& f = fb.build(0, 1000.0);
dec.beginFrame(f);
std::vector<double> ts;
ASSERT_TRUE(dec.timestamps(f, 0, ts));
ASSERT_EQ(ts.size(), 4u);
/* Element 0 lands on the arrival time; the rest keep the producer spacing. */
EXPECT_NEAR(ts[0], 1000.000, 1e-9);
EXPECT_NEAR(ts[1], 1000.001, 1e-9);
EXPECT_NEAR(ts[2], 1000.002, 1e-9);
EXPECT_NEAR(ts[3], 1000.003, 1e-9);
}
TEST(FrameDecoder, FirstSampleAnchorsElementZeroAndCountsForward) {
FrameDecoder dec;
dec.setSignals({burst("Sine", kTimeFirstSample, 1000.0, 4, 1),
timeSignal("Time", 1)});
FrameBuilder fb;
fb.addSignal({1.0, 2.0, 3.0, 4.0});
fb.addSignal({7.0e9});
const FrameView& f = fb.build(0, 2000.0);
dec.beginFrame(f);
std::vector<double> ts;
ASSERT_TRUE(dec.timestamps(f, 0, ts));
ASSERT_EQ(ts.size(), 4u);
EXPECT_NEAR(ts[0], 2000.000, 1e-9);
EXPECT_NEAR(ts[3], 2000.003, 1e-9);
}
TEST(FrameDecoder, LastSampleAnchorsTheFinalElementAndCountsBackward) {
FrameDecoder dec;
dec.setSignals({burst("Sine", kTimeLastSample, 1000.0, 4, 1),
timeSignal("Time", 1)});
FrameBuilder fb;
fb.addSignal({1.0, 2.0, 3.0, 4.0});
fb.addSignal({7.0e9});
const FrameView& f = fb.build(0, 3000.0);
dec.beginFrame(f);
std::vector<double> ts;
ASSERT_TRUE(dec.timestamps(f, 0, ts));
ASSERT_EQ(ts.size(), 4u);
EXPECT_NEAR(ts[3], 3000.000, 1e-9);
EXPECT_NEAR(ts[0], 3000.000 - 0.003, 1e-9);
}
TEST(FrameDecoder, PlainScalarUsesArrivalTime) {
FrameDecoder dec;
SignalMeta m;
m.name = "Level";
m.typeCode = 9;
dec.setSignals({m});
FrameBuilder fb;
fb.addSignal({42.0});
const FrameView& f = fb.build(0, 1234.5);
dec.beginFrame(f);
std::vector<double> ts;
ASSERT_TRUE(dec.timestamps(f, 0, ts));
ASSERT_EQ(ts.size(), 1u);
EXPECT_DOUBLE_EQ(ts[0], 1234.5);
}
// This is the failure UDPSourceSession.cpp:560 documents. The kernel delivers
// two queued datagrams microseconds apart even though each carries 10 ms of
// signal. Dating from arrival crams the second packet's samples into that gap
// and the trace becomes a sawtooth; dating from the producer hrt does not.
TEST(FrameDecoder, AccumulatedScalarSurvivesBurstyDelivery) {
FrameDecoder dec;
SignalMeta m;
m.name = "Acc";
m.typeCode = 9;
m.numRows = 1;
m.samplingRate = 1000.0; /* 1 kHz, 10 samples = 10 ms per packet */
dec.setSignals({m});
const double ticks = 1.0e9;
std::vector<double> all;
for (int p = 0; p < 40; p++) {
FrameBuilder fb;
fb.addSignal(std::vector<double>(10, static_cast<double>(p)));
const double producerSec = 100.0 + p * 0.010;
/* Packets 20+ arrive in a burst, all within 50 us of each other. */
const double arrival = (p < 20) ? (500.0 + p * 0.010)
: (500.2 + (p - 20) * 0.00005);
const FrameView& f = fb.build(static_cast<uint64_t>(producerSec * ticks),
arrival, 10);
dec.beginFrame(f);
std::vector<double> ts;
if (dec.timestamps(f, 0, ts)) {
all.insert(all.end(), ts.begin(), ts.end());
}
}
ASSERT_GT(all.size(), 300u);
for (size_t i = 1; i < all.size(); i++) {
EXPECT_GT(all[i], all[i - 1]) << "non-monotonic at " << i;
EXPECT_NEAR(all[i] - all[i - 1], 0.001, 2e-4)
<< "spacing collapsed at " << i << " (sawtooth)";
}
}
// The counterweight to the test above. Suppressing arrival jitter by chaining
// each burst onto the previous one is only safe while the chain is checked: on
// UDP, packets are lost, and a chain that ignores arrival entirely closes the
// hole silently and dates every later sample a full second early — for the rest
// of the run, because nothing ever pulls it back. The prediction has to be
// abandoned once arrival contradicts it by more than a delivery backlog could.
TEST(FrameDecoder, AccumulatedScalarResynchronisesAfterLostPackets) {
FrameDecoder dec;
SignalMeta m;
m.name = "Acc";
m.typeCode = 9;
m.numRows = 1;
m.samplingRate = 1000.0; /* 10 samples = 10 ms per packet */
dec.setSignals({m});
std::vector<double> ts;
for (int p = 0; p < 10; p++) {
FrameBuilder fb;
fb.addSignal(std::vector<double>(10, 1.0));
const FrameView& f = fb.build(0, 500.0 + p * 0.010, 10);
dec.beginFrame(f);
ASSERT_TRUE(dec.timestamps(f, 0, ts));
}
/* Contiguous so far: burst 9 ends at 500.090. */
EXPECT_NEAR(ts[9], 500.090, 1e-9);
/* A full second of packets never arrives. The next one lands at 501.100. */
FrameBuilder fb;
fb.addSignal(std::vector<double>(10, 1.0));
const FrameView& f = fb.build(0, 501.100, 10);
dec.beginFrame(f);
ASSERT_TRUE(dec.timestamps(f, 0, ts));
/* Chaining blindly would put this burst at 500.091..500.100, overlapping
* the gap as though no data were missing. */
EXPECT_NEAR(ts[0], 501.091, 1e-9);
EXPECT_NEAR(ts[9], 501.100, 1e-9);
}
TEST(FrameDecoder, AccumulatedScalarDerivesDtFromTheHrtGapWhenNoRateIsDeclared) {
FrameDecoder dec;
SignalMeta m;
m.name = "Acc";
m.typeCode = 9;
m.samplingRate = 0.0; /* undeclared */
dec.setSignals({m});
const double ticks = 1.0e9;
std::vector<double> last;
for (int p = 0; p < 40; p++) {
FrameBuilder fb;
fb.addSignal(std::vector<double>(10, 1.0));
const double producerSec = 100.0 + p * 0.010; /* 10 ms per packet */
const FrameView& f = fb.build(static_cast<uint64_t>(producerSec * ticks),
700.0 + p * 0.010, 10);
dec.beginFrame(f);
std::vector<double> ts;
if (dec.timestamps(f, 0, ts)) { last = ts; }
}
ASSERT_EQ(last.size(), 10u);
/* 10 ms of producer time across 10 samples is a 1 ms period. */
EXPECT_NEAR(last[1] - last[0], 0.001, 1e-5);
}
// A PACKET burst has no per-element time at all. Elements span
// (lastPacket, thisPacket] — backwards from arrival, because the samples were
// acquired before the packet landed. Forward extrapolation would let a jittered
// packet overlap the next one and break ring monotonicity.
TEST(FrameDecoder, PacketBurstDropsTheFirstFrameThenSpansBackwards) {
FrameDecoder dec;
dec.setSignals({burst("Raw", kTimePacket, 0.0, 5, kNoTimeSignal)});
FrameBuilder fb1;
fb1.addSignal({1.0, 2.0, 3.0, 4.0, 5.0});
const FrameView& f1 = fb1.build(0, 10.0);
dec.beginFrame(f1);
std::vector<double> ts;
EXPECT_FALSE(dec.timestamps(f1, 0, ts))
<< "the first packet has no previous arrival to span from";
FrameBuilder fb2;
fb2.addSignal({6.0, 7.0, 8.0, 9.0, 10.0});
const FrameView& f2 = fb2.build(0, 10.05);
dec.beginFrame(f2);
ASSERT_TRUE(dec.timestamps(f2, 0, ts));
ASSERT_EQ(ts.size(), 5u);
EXPECT_GT(ts[0], 10.0);
EXPECT_NEAR(ts[4], 10.05, 1e-12);
EXPECT_NEAR(ts[1] - ts[0], 0.01, 1e-12);
}
TEST(FrameDecoder, PacketBurstStaysMonotonicUnderJitteredArrivals) {
FrameDecoder dec;
dec.setSignals({burst("Raw", kTimePacket, 0.0, 8, kNoTimeSignal)});
const double jitter[] = {0.0, 0.004, -0.003, 0.006, -0.002, 0.0, 0.005, -0.004};
std::vector<double> all;
for (int p = 0; p < 8; p++) {
FrameBuilder fb;
fb.addSignal(std::vector<double>(8, 1.0));
const FrameView& f = fb.build(0, 20.0 + p * 0.05 + jitter[p]);
dec.beginFrame(f);
std::vector<double> ts;
if (dec.timestamps(f, 0, ts)) {
all.insert(all.end(), ts.begin(), ts.end());
}
}
ASSERT_GT(all.size(), 8u);
for (size_t i = 1; i < all.size(); i++) {
EXPECT_GT(all[i], all[i - 1]) << "packets overlapped at " << i;
}
}
TEST(FrameDecoder, ResetForgetsPerSignalHistory) {
FrameDecoder dec;
dec.setSignals({burst("Raw", kTimePacket, 0.0, 4, kNoTimeSignal)});
FrameBuilder fb;
fb.addSignal({1.0, 2.0, 3.0, 4.0});
const FrameView& f = fb.build(0, 5.0);
dec.beginFrame(f);
std::vector<double> ts;
EXPECT_FALSE(dec.timestamps(f, 0, ts));
const FrameView& f2 = fb.build(0, 5.1);
dec.beginFrame(f2);
EXPECT_TRUE(dec.timestamps(f2, 0, ts));
dec.reset();
const FrameView& f3 = fb.build(0, 5.2);
dec.beginFrame(f3);
EXPECT_FALSE(dec.timestamps(f3, 0, ts))
<< "after reset the next packet is again the first one";
}
+230
View File
@@ -0,0 +1,230 @@
#include "PaneTree.h"
#include <gtest/gtest.h>
using namespace udpscope;
namespace {
const Rect kScreen{0.0, 0.0, 1000.0, 600.0};
std::vector<PaneTree::Placed> leavesOf(const PaneTree& tree, const Rect& area) {
std::vector<PaneTree::Placed> leaves;
std::vector<PaneTree::Splitter> splitters;
tree.layout(area, leaves, splitters);
return leaves;
}
} /* namespace */
TEST(PaneTree, StartsAsOneEmptyLeafFillingTheArea) {
PaneTree tree;
EXPECT_EQ(tree.leafCount(), 1u);
const auto leaves = leavesOf(tree, kScreen);
ASSERT_EQ(leaves.size(), 1u);
EXPECT_DOUBLE_EQ(leaves[0].rect.w, 1000.0);
EXPECT_DOUBLE_EQ(leaves[0].rect.h, 600.0);
EXPECT_TRUE(leaves[0].leaf->signals.empty());
}
TEST(PaneTree, SplittingIntoColumnsHalvesTheWidth) {
PaneTree tree;
tree.splitLeaf(tree.root(), Orient::Columns);
const auto leaves = leavesOf(tree, kScreen);
ASSERT_EQ(leaves.size(), 2u);
EXPECT_DOUBLE_EQ(leaves[0].rect.w, 500.0);
EXPECT_DOUBLE_EQ(leaves[1].rect.w, 500.0);
EXPECT_DOUBLE_EQ(leaves[0].rect.h, 600.0);
EXPECT_DOUBLE_EQ(leaves[1].rect.x, 500.0);
}
TEST(PaneTree, SplittingIntoRowsHalvesTheHeight) {
PaneTree tree;
tree.splitLeaf(tree.root(), Orient::Rows);
const auto leaves = leavesOf(tree, kScreen);
ASSERT_EQ(leaves.size(), 2u);
EXPECT_DOUBLE_EQ(leaves[0].rect.h, 300.0);
EXPECT_DOUBLE_EQ(leaves[1].rect.y, 300.0);
EXPECT_DOUBLE_EQ(leaves[0].rect.w, 1000.0);
}
// The pane being split keeps its content; the new pane is the empty one.
TEST(PaneTree, SplitKeepsTheOriginalContentInTheFirstChild) {
PaneTree tree;
tree.root()->signals.push_back(Assignment{"Voltage", Color{}, 1.5f, VScale{}});
tree.splitLeaf(tree.root(), Orient::Columns);
const auto leaves = leavesOf(tree, kScreen);
ASSERT_EQ(leaves.size(), 2u);
ASSERT_EQ(leaves[0].leaf->signals.size(), 1u);
EXPECT_EQ(leaves[0].leaf->signals[0].signalName, "Voltage");
EXPECT_TRUE(leaves[1].leaf->signals.empty());
}
TEST(PaneTree, ClosingALeafGivesItsSpaceToTheSibling) {
PaneTree tree;
tree.splitLeaf(tree.root(), Orient::Columns);
auto leaves = leavesOf(tree, kScreen);
ASSERT_EQ(leaves.size(), 2u);
leaves[1].leaf->signals.push_back(Assignment{"Keep", Color{}, 1.5f, VScale{}});
tree.closeLeaf(leaves[0].leaf);
EXPECT_EQ(tree.leafCount(), 1u);
leaves = leavesOf(tree, kScreen);
ASSERT_EQ(leaves.size(), 1u);
EXPECT_DOUBLE_EQ(leaves[0].rect.w, 1000.0);
ASSERT_EQ(leaves[0].leaf->signals.size(), 1u);
EXPECT_EQ(leaves[0].leaf->signals[0].signalName, "Keep");
}
TEST(PaneTree, RefusesToCloseTheLastLeaf) {
PaneTree tree;
tree.closeLeaf(tree.root());
EXPECT_EQ(tree.leafCount(), 1u);
}
// A pane in the middle of a 3x3 touches no window edge. It must still be
// splittable, which is why handles are inset inside the pane rather than
// keyed on the window border.
TEST(PaneTree, AnInteriorPaneIsStillSplittable) {
PaneTree tree;
tree.splitLeaf(tree.root(), Orient::Rows); // top / bottom
auto leaves = leavesOf(tree, kScreen);
tree.splitLeaf(leaves[1].leaf, Orient::Rows); // 3 rows
leaves = leavesOf(tree, kScreen);
ASSERT_EQ(leaves.size(), 3u);
PaneNode* middle = leaves[1].leaf;
tree.splitLeaf(middle, Orient::Columns);
leaves = leavesOf(tree, kScreen);
tree.splitLeaf(leaves[2].leaf, Orient::Columns);
EXPECT_EQ(tree.leafCount(), 5u);
}
TEST(PaneTree, LayoutReportsOneSplitterPerSplitNode) {
PaneTree tree;
tree.splitLeaf(tree.root(), Orient::Columns);
auto leaves = leavesOf(tree, kScreen);
tree.splitLeaf(leaves[0].leaf, Orient::Rows);
std::vector<PaneTree::Placed> out;
std::vector<PaneTree::Splitter> splitters;
tree.layout(kScreen, out, splitters);
EXPECT_EQ(out.size(), 3u);
EXPECT_EQ(splitters.size(), 2u);
}
TEST(PaneTree, RatioSurvivesALayoutRoundTrip) {
PaneTree tree;
tree.splitLeaf(tree.root(), Orient::Columns);
tree.setRatio(tree.root(), 0.25);
const auto leaves = leavesOf(tree, kScreen);
ASSERT_EQ(leaves.size(), 2u);
EXPECT_DOUBLE_EQ(leaves[0].rect.w, 250.0);
EXPECT_DOUBLE_EQ(leaves[1].rect.w, 750.0);
}
TEST(PaneTree, RatioIsClampedSoNeitherPaneGoesBelowTheMinimum) {
PaneTree tree;
tree.splitLeaf(tree.root(), Orient::Columns);
tree.setRatio(tree.root(), 0.001);
const auto leaves = leavesOf(tree, kScreen);
EXPECT_GE(leaves[0].rect.w, kMinPaneSize);
EXPECT_GE(leaves[1].rect.w, kMinPaneSize);
}
TEST(PaneTree, HitTestFindsTheSplitterBetweenTwoPanes) {
PaneTree tree;
tree.splitLeaf(tree.root(), Orient::Columns);
std::vector<PaneTree::Placed> leaves;
std::vector<PaneTree::Splitter> splitters;
tree.layout(kScreen, leaves, splitters);
ASSERT_EQ(splitters.size(), 1u);
const PaneTree::Splitter* hit = tree.hitTestSplitter(splitters, 500.0, 300.0);
ASSERT_NE(hit, nullptr);
EXPECT_EQ(hit->orient, Orient::Columns);
EXPECT_EQ(tree.hitTestSplitter(splitters, 100.0, 300.0), nullptr);
}
TEST(PaneTree, HitTestFindsInsetSplitHandlesAndTheCloseButton) {
const Rect pane{0.0, 0.0, 400.0, 300.0};
EXPECT_EQ(PaneTree::hitTestHandle(pane, 8.0, 150.0), Handle::Left);
EXPECT_EQ(PaneTree::hitTestHandle(pane, 392.0, 150.0), Handle::Right);
EXPECT_EQ(PaneTree::hitTestHandle(pane, 200.0, 8.0), Handle::Top);
EXPECT_EQ(PaneTree::hitTestHandle(pane, 200.0, 292.0), Handle::Bottom);
EXPECT_EQ(PaneTree::hitTestHandle(pane, 392.0, 8.0), Handle::Close);
EXPECT_EQ(PaneTree::hitTestHandle(pane, 200.0, 150.0), Handle::None);
}
// Gap 1: closeLeaf only tested with first child closed; test closing the second child.
TEST(PaneTree, ClosingTheSecondLeafPreservesTheFirstLeafContent) {
PaneTree tree;
tree.splitLeaf(tree.root(), Orient::Columns);
auto leaves = leavesOf(tree, kScreen);
ASSERT_EQ(leaves.size(), 2u);
// Assign distinct signals to each leaf
leaves[0].leaf->signals.push_back(Assignment{"Signal_A", Color{}, 1.5f, VScale{}});
leaves[1].leaf->signals.push_back(Assignment{"Signal_B", Color{}, 1.5f, VScale{}});
// Close the second leaf; the first should survive with its content
tree.closeLeaf(leaves[1].leaf);
EXPECT_EQ(tree.leafCount(), 1u);
leaves = leavesOf(tree, kScreen);
ASSERT_EQ(leaves.size(), 1u);
ASSERT_EQ(leaves[0].leaf->signals.size(), 1u);
EXPECT_EQ(leaves[0].leaf->signals[0].signalName, "Signal_A");
}
// Gap 2: closeLeaf only tested at depth 1; test at depth 2 (deeper recursion in findParent).
TEST(PaneTree, ClosingALeafAtDepth2PreservesOthersAndUpdatesCount) {
PaneTree tree;
// Build tree: split root (a, b), split b to get depth-2 leaf in the RIGHT subtree
tree.splitLeaf(tree.root(), Orient::Columns); // depth 1: root splits into a, b
auto leaves = leavesOf(tree, kScreen);
ASSERT_EQ(leaves.size(), 2u);
tree.splitLeaf(leaves[1].leaf, Orient::Rows); // depth 2: b splits into b.a, b.b
leaves = leavesOf(tree, kScreen);
ASSERT_EQ(leaves.size(), 3u);
// Assign distinct signals to each of the three leaves
leaves[0].leaf->signals.push_back(Assignment{"Depth1_Left", Color{}, 1.5f, VScale{}});
leaves[1].leaf->signals.push_back(Assignment{"Depth2_TopRight", Color{}, 1.5f, VScale{}});
leaves[2].leaf->signals.push_back(Assignment{"Depth2_BottomRight", Color{}, 1.5f, VScale{}});
// Close the first depth-2 leaf (leaves[1], which is in the right subtree)
tree.closeLeaf(leaves[1].leaf);
EXPECT_EQ(tree.leafCount(), 2u);
leaves = leavesOf(tree, kScreen);
ASSERT_EQ(leaves.size(), 2u);
// Verify the surviving depth-2 leaf has its signal intact
bool found_left = false;
bool found_bottom_right = false;
for (const auto& leaf : leaves) {
ASSERT_EQ(leaf.leaf->signals.size(), 1u);
if (leaf.leaf->signals[0].signalName == "Depth1_Left") {
found_left = true;
}
if (leaf.leaf->signals[0].signalName == "Depth2_BottomRight") {
found_bottom_right = true;
}
}
EXPECT_TRUE(found_left);
EXPECT_TRUE(found_bottom_right);
}
+117
View File
@@ -0,0 +1,117 @@
#include "TimeBase.h"
#include <gtest/gtest.h>
using namespace udpscope;
TEST(ClockOffset, MapsTheFirstReadingOntoWallClockExactly) {
ClockOffset off;
EXPECT_FALSE(off.valid());
const double wall = 1756291200.5;
EXPECT_DOUBLE_EQ(off.map(10.0, wall), wall);
EXPECT_TRUE(off.valid());
}
// Network delay jitters the arrival time. If the offset chased every packet
// the whole trace would wobble, so it is latched and only corrected on real
// drift.
TEST(ClockOffset, HoldsTheOffsetThroughSmallArrivalJitter) {
ClockOffset off;
off.map(10.0, 1000.0); // offset = 990
// Arrival wanders either side of the prediction. wallSec is a local receive
// timestamp, so it only ever advances — jitter shows up as the gap growing
// and shrinking, never as the clock going backwards.
EXPECT_DOUBLE_EQ(off.map(11.0, 1001.02), 1001.0); // +0.02 late
EXPECT_DOUBLE_EQ(off.map(12.0, 1001.97), 1002.0); // -0.03 early
}
// The threshold has to be symmetric. A producer whose clock steps FORWARD (an
// NTP correction on the producer's host, say) puts the prediction permanently
// ahead of the wall clock — a one-sided "recalibrate only when wall is ahead"
// test never fires for it, and the trace sits in the future for the rest of the
// run.
TEST(ClockOffset, RecalibratesWhenTheProducerClockJumpsForward) {
ClockOffset off;
off.map(10.0, 1000.0); // offset = 990
// Producer leaps 100 s ahead while only 1 s of wall time passes.
EXPECT_DOUBLE_EQ(off.map(111.0, 1001.0), 1001.0);
}
TEST(ClockOffset, RecalibratesWhenDriftExceedsTheThreshold) {
ClockOffset off;
off.map(10.0, 1000.0); // offset = 990
/* Producer clock jumped (restart, re-phase): 5 s of error is not jitter. */
const double mapped = off.map(11.0, 1006.0);
EXPECT_DOUBLE_EQ(mapped, 1006.0);
}
TEST(ClockOffset, ResetForgetsTheCalibration) {
ClockOffset off;
off.map(10.0, 1000.0);
off.reset();
EXPECT_FALSE(off.valid());
EXPECT_DOUBLE_EQ(off.map(50.0, 2000.0), 2000.0);
}
// The tick rate of the producer's high-resolution timer is not carried by the
// protocol, and StreamHub's trick of using the local MARTe timer frequency only
// works on the producer's own host. Recover it from the data instead.
TEST(HrtRateFit, RecoversAKnownTickRate) {
HrtRateFit fit;
const double ticksPerSec = 2.5e9;
EXPECT_FALSE(fit.ready());
for (int i = 0; i < 64; i++) {
const double wall = 1000.0 + i * 0.01;
fit.add(static_cast<uint64_t>(wall * ticksPerSec), wall);
}
ASSERT_TRUE(fit.ready());
EXPECT_NEAR(fit.ticksPerSecond(), ticksPerSec, ticksPerSec * 1e-6);
}
TEST(HrtRateFit, IsNotReadyBeforeTheMinimumSampleCount) {
HrtRateFit fit;
for (size_t i = 0; i < HrtRateFit::kMinSamples - 1; i++) {
fit.add(static_cast<uint64_t>(i) * 1000000u, 1000.0 + i * 0.001);
}
EXPECT_FALSE(fit.ready());
fit.add(static_cast<uint64_t>(HrtRateFit::kMinSamples) * 1000000u,
1000.0 + HrtRateFit::kMinSamples * 0.001);
EXPECT_TRUE(fit.ready());
}
TEST(HrtRateFit, ToSecondsUsesTheFittedRate) {
HrtRateFit fit;
const double ticksPerSec = 1.0e9;
for (int i = 0; i < 64; i++) {
const double wall = 500.0 + i * 0.005;
fit.add(static_cast<uint64_t>(wall * ticksPerSec), wall);
}
ASSERT_TRUE(fit.ready());
EXPECT_NEAR(fit.toSeconds(2000000000ull), 2.0, 1e-4);
}
TEST(HrtRateFit, SurvivesAStalledClock) {
HrtRateFit fit;
for (int i = 0; i < 64; i++) {
fit.add(12345u, 1000.0 + i * 0.01); /* hrt never advances */
}
/* A degenerate fit must not produce a rate that would divide by zero, so it
* must decline to be ready at all. Guarding this behind `if (fit.ready())`
* would make the test vacuous: the branch never runs and a fit that
* declared itself ready with a rate of 0 or NaN would pass unnoticed. */
EXPECT_FALSE(fit.ready());
EXPECT_DOUBLE_EQ(fit.ticksPerSecond(), 0.0);
}
TEST(TimeSignalScale, UsesNanosecondsForUint64AndMicrosecondsOtherwise) {
EXPECT_DOUBLE_EQ(TimeSignalScale(6 /* UDPS_T_UINT64 */), 1.0e-9);
EXPECT_DOUBLE_EQ(TimeSignalScale(9 /* UDPS_T_FLOAT64 */), 1.0e-6);
EXPECT_DOUBLE_EQ(TimeSignalScale(4 /* UDPS_T_UINT32 */), 1.0e-6);
}
-425
View File
@@ -1,425 +0,0 @@
# Buffer time-window & trigger logic in `Client/udpstreamer`
How the UDP Scope client acquires, buffers, times and triggers waveforms.
The pipeline has two halves that must be read together:
- the **Go hub** (`Common/Client/go/wshub/`) — owns the UDP sockets, the
full-resolution sample storage, the disk history, and the trigger FSM;
- the **browser SPA** (`static/app.js`) — owns the display buffers, the rolling
window, and the trigger capture rendering.
The same SPA is also served by `Client/webui` and talks to the C++ StreamHub,
which mirrors the Go hub's behaviour (same trigger FSM states, same binary
frames). Everything below describes the Go-hub path; the wire contracts are
identical on both.
---
## 1. End-to-end data flow
```
MARTe2 RT app ──UDP/UDPS──▶ sources.go: runSession()
│ CONFIG + DATA packets, 17-byte header, HRT timestamp per frame
▼
udpsprotocol.ParseData() → []DataSample{HRTTimestamp, WallTime, Values}
▼
Hub.Run() dataCh → pending[sourceID] (drained every 30 Hz tick)
▼
buildBinaryDataMessageForSource()
├─ rebuild per-sample timestamps from TimeMode / calibration / monotonic snap
├─ h.ingest(key, n, t, v) ← FULL rate: ring.write + hist.write + trigger.feed
└─ minMaxDecimate(…, maxPushPoints=50) → WS binary v1 frame to clients
▼
browser: onBinaryData() → pushBuffer() into per-signal circular buffers
▼
renderDirtyPlots() (rAF loop) → buildUPlotData() → uPlot
```
The 30 Hz push is the **only** live path to the browser and it is decimated to
≤50 points/signal/tick. Everything that needs full resolution — zoom, trigger
captures, disk history — is fed independently through `ingest()` and never goes
over the wire until asked.
---
## 2. Hub-side buffers: `sigRing` (`wshub/ringbuf.go`)
One ring per `"sourceId:signalName"` key. A fixed-capacity circular buffer of
Float64 `(t, v)` pairs with a `sync.RWMutex` (writes from `Hub.Run()`, reads
from HTTP/WS handler goroutines).
### 2.1 Min/max bucketing
`bucket` is how many source samples collapse into **one min/max pair** on the
way in:
- `bucket == 1` — the stream is stored verbatim;
- `bucket > 1` — each group contributes its minimum and its maximum, emitted in
time order (`flushBucketLocked`), so the stored timestamps stay
non-decreasing (reads binary-search `rb.t`).
Bucketing is what lets an arbitrarily long window fit a fixed per-signal memory
budget at a megasample rate. Samples already stored keep the resolution they
were written at; the ring converges on a new bucket as it rolls
(`setBucket`).
### 2.2 Source-rate measurement
`sigRing` keeps its own source-sample accounting (`srcCount`, `srcT0`, `srcT1`,
reset every `srcRateWindowSec = 10 s`), because once `bucket > 1` neither `size`
nor the stored timespan measures the real incoming rate. `sourceRate()` is used
by the tuning sweep and by the history writer.
### 2.3 Ring tuning (`retuneRings`, every 1 s)
`activeWindowSec()` decides how far back the rings must reach:
1. an **armed trigger** owns the window: `cfg.windowSec + captureLagSec`
(`captureLagSec = captureMarginSec + 1/30 ≈ 0.183 s` — the capture is read
out a post-window + margin + one push tick after the trigger, so the rings
must hold that much extra or the front of the capture has already rolled);
2. otherwise the **widest window any connected client is displaying**
(`wsClient.displayWindowSec`, set by the SPA's `setWindow` command), with a
`defaultLiveWindowSec = 10 s` fallback while nobody has said;
Then per ring, with `budget = ringBudget()` (default `defaultRingPts = 10 M`,
floor `ringCapInitial = 250 k`):
- grow to the budget first (`grow()` preserves all samples, never shrinks);
- compute the needed bucket with `ringBucketFor(rate, window, capacity)`
(`ceil(2·rate·window·ringHeadroom / capacity)`, `ringHeadroom = 1.25`);
- apply it with **hysteresis**: keep the current bucket while its coverage is
between `need` and `2·need`, so a rate jittering across the boundary does not
flip the resolution every second.
The history archive is re-sized from the same window (`hist.setWindow`) so a
zoom or capture that outlives the rings can fall back to it.
### 2.4 Reading: `slice(t0, t1)`
Binary search for `t0` then `t1` over the circular layout, returning copies of
the pairs in `[t0, t1]`. Safe to use without holding the lock.
---
## 3. Disk history (`wshub/history.go`)
Optional (`EnableHistory`, `CloseHistory`), enabled by the hub configuration.
Every sample goes to disk through `ingest → hist.write` at full rate, in files
sized for the *current* window (not a retention period). It exists to back
three things the rings cannot:
- **zoom past the window**: `readRange(key, t0, t1, maxOut)`;
- **captures the rings have rolled past**: `captureRange(trigTime−pre, trigTime+post)`
lifts each capture into a file of its own so nothing overwrites it before the
next trigger;
- **short captures**: `backfillCaptureHead` prepends the front of the window the
ring no longer holds (the ring only *becomes* as long as the window after a
re-tune; the archive was written straight through).
---
## 4. Live push to the browser
`Hub.Run()` drains `pending[sourceID]` on a 30 Hz ticker. Even with no client
connected the frame is built: that is what keeps feeding rings, history and the
trigger, and keeps push cursors advancing so a late client does not get a
backlog burst.
`buildBinaryDataMessageForSource` reconstructs per-sample timestamps per signal
`TimeMode`:
| Mode | Timestamp reconstruction |
|---|---|
| `FirstSample` / `LastSample` | scalar TimeSignal value × `timerToSec` (µs→s or ns→s for u64), calibrated once against `WallTime`; samples spaced by `1/SamplingRate` |
| `FullArray` | per-element TimeSignal array, calibrated once against `WallTime` |
| scalar (`n == 1`) | `WallTime` of the UDP arrival |
| `PacketTime` (default, n>1) | inter-packet wall-clock gaps divided by n (single-packet ticks use the gap from the previous tick) |
**Monotonic snapping** (optional, `setMonotonic` command / "Sync TS" checkbox):
when enabled, the inter-frame anchor gap is smoothed with an EMA
(`monotonicEMAAlpha = 0.01`, initialised from the nominal `n·dt`) and small
deviations (< `monotonicTolerance = 5 ms`) are snapped to the smoothed gap,
removing the software-dispatch jitter overlaps/gaps described in the StreamHub
docs while tracking the true hardware rate (no accumulated drift).
The live frame is a **binary v1** WS message:
```
[u8 1][u8 srcIdLen][srcId][u32 nSigs]
{[u16 keyLen][key][u32 N][f64 t×N][f64 v×N]}
```
with each signal min/max-decimated to `maxPushPoints = 50` (`minMaxDecimate`:
the range is split into `threshold/2` buckets, each contributing its min and max
in time order — a scope-style envelope that keeps glitches on screen).
---
## 5. Browser-side buffers (`static/app.js`)
### 5.1 Capacity & growth
- `MAX_CAP = 2 000 000` — hard ceiling per buffer (~32 MB/signal at Float64 t+v);
- `DEFAULT_CAP = 100 000` — starting size for scalars;
- `TEMPORAL_CAP = 500 000` — starting size for array signals (the hub pushes
≤50 pts/signal/tick, so this already covers ~5 min);
- `growBufferForWindow(buf, windowSec)` — **sizes from the buffer's own span**,
not the signal's sampling rate: the incoming rate here is the hub-decimated
~1.5 kpts/s regardless of the source rate, so rate-based sizing overshot by
three orders of magnitude. Grows only when the buffer is full, to
`windowSec × 1.5` headroom, capped at `MAX_CAP`.
- `growBuffer` copies all existing samples into a larger array (preserving
circular order).
### 5.2 The window
`windowSec` (default 5 s, options 1 s … 10 min) is the rolling viewport.
Changing it:
1. updates `windowSec`;
2. `sendWindow()` → WS `setWindow` → hub `displayWindowSec` → ring re-tune;
3. grows every local buffer via `growBufferForWindow`;
4. evicts the decimation cache (a different window invalidates all cached
renderings).
The rolling "now" anchor is **data-driven, not wall-clock**:
`computePlotNow(p)` takes the newest timestamp of each contributing source and
uses the min-of-max over sources that are still active (a source lagging the
fastest by more than `windowSec` is treated as stale and excluded). This keeps
the window tracking real data regardless of clock skew between hub and browser.
### 5.3 Slicing & rendering
- `getBufferSliceRange(buf, t0, t1)` — binary search on the circular layout,
O(log n + window size);
- `getBufferSliceRangeWithBrackets` — same plus one point on each side so lines
still cross a nearly-empty zoom window;
- `supplementWithBrackets` — same bracketing for sparse server-fetched zoom data.
`buildLiveData(p)`:
1. slices every trace in `[t0, t1]`;
2. picks the **master** signal: highest `SamplingRate`, then most points;
3. decimates the master to ~2× plot width (`DECIM_MIN = 200` floor) via a
background worker (`decimateAsync`, stale-while-revalidate cache keyed per
plot/range/data-generation);
4. resamples every other trace onto the master grid with `resampleLinear`;
5. normalises Y (`applyVScaleNorm`: calibration `v·scale+offset`, then
`(y − offset)/div`).
### 5.4 Zoom
A zoom pins `p.xRange` and asks the hub for hi-res data over the exact range
(WS `zoom` request or HTTP `/api/zoom`). The hub answers from the full-res
rings — or from the **held copy of the last trigger capture** (`captureHold`
double buffer) while that window is still relevant — decimated to the requested
point budget. The browser prefers the fetched data when it exists, falls back
to its own circular buffers otherwise, and always brackets with local points.
---
## 6. Hub-side trigger FSM (`wshub/trigger.go`)
### 6.1 States and configuration
```
idle ──arm──▶ armed ──edge──▶ collecting ──window elapsed──▶ triggered
▲ ▲ (pre/post latched) │
│ └───────────── rearm (normal mode, after holdoff) ◀─────────┘
└────────────── disarm / single mode stays triggered
```
Configuration (`trigConfig`, client-settable via WS `setTrigger`):
| field | meaning | clamp |
|---|---|---|
| `signalKey` | `"src:sig"` or `"src:sig[i]"` | — |
| `edge` | `rising` / `falling` / `both` | — |
| `threshold` | **raw** units (SPA converts calibrated → raw) | — |
| `windowSec` | capture window | `[1e-4, 600]` |
| `prePercent` | pre-trigger share | `[0, 100]` |
| `mode` | `normal` (auto-rearm) / `single` | — |
| `holdoffSec` | re-arm delay after a capture, double-trigger guard | `[0, 60]` |
### 6.2 Edge detection (`feed`)
Called from `ingest` with every full-resolution batch for the trigger signal.
Level tracking (`prevValue`/`prevValid`) compares consecutive samples against
the threshold; `[i]`-suffixed keys stride the flattened batch by `nElem` to
watch one column. On a qualifying edge in `armed` state: `latchWindowLocked`
freezes `trigTime` and the pre/post split (so later config edits cannot move a
capture's axis).
### 6.3 Buffer-fill gate
Before accepting an edge, the FSM checks that the trigger signal's ring reaches
back far enough that the capture will come back whole (`fillLocked`):
```
need = windowSec − growth × postSec, floored at the pre-window
```
`growth` is the measured span-growth rate of the ring (`setBuffered`, refreshed
by `refreshTriggerFill` from the tick and from trigger commands). A still-filling
ring grows 1 s of span per second, so the gate reduces to the pre-window; a
full ring at a long window needs the whole window. While holding off, the level
is still tracked so the first edge after the gate opens is measured against the
right predecessor. The SPA shows the hold-off as an armed trigger with a
`bufferFill %` badge.
### 6.4 Window timing and the pending edge
`dueCapture` waits for the window on the **sample clock**, not the wall clock:
`lastT ≥ trigTime + post + captureMarginSec(0.15)`. This avoids cutting a
capture short when the stream's timestamps lag real time. Three ways it fires:
1. the samples themselves covered the window;
2. wall-clock fallback when no sample was ever seen (Force from idle);
3. `captureStallSec = 2 s` of stream silence — deliver what was collected
rather than leaving the client stuck in "collecting".
While a capture is in flight the comparator keeps running. The **first**
qualifying edge at/after `notBefore = trigTime + max(post, holdoffSec)` is
remembered (`pendingT`/`pendingValid`) and fired immediately on the automatic
`rearm()`. Without this the trigger was deaf through the whole post-window +
holdoff, which rounded sparse pulse trains up to whole periods (a 1 Hz train at
a 1 s window was caught at 0.5 Hz).
### 6.5 Holdoff and rearm
`markTriggered` moves `collecting → triggered` and, in `normal` mode (not
stopped), schedules `rearmAt = now + cfg.holdoffSec`. `dueRearm` consumes it;
`rearm()` re-arms immediately on a pending edge or returns to `armed`. The
holdoff is measured from the trigger point, overlapping the post-window rather
than adding to it.
`Force()` fires immediately at the most recent sample time (wall clock if no
sample yet) — the "Force" button.
### 6.6 Capture assembly (`buildTriggerCapture`)
On a due capture the hub builds the **binary v2** frame:
```
[u8 2][f64 trigTime][f64 preSec][f64 postSec][u32 nSig]
{[u16 keyLen][fullKey][u32 N][f64 t×N][f64 v×N]}
```
For every ring:
1. `slice(trigTime−pre, trigTime+post)`;
2. `backfillCaptureHead` from disk history for the front the ring lost;
3. if it is still short by more than `shortCaptureTol = 1 %` of the window,
log it explicitly (nothing can recover data the ring never held);
4. keep the **full-resolution** slice in the `captureHold` double buffer
(so a zoom into the capture can be answered after the rings roll past);
5. min/max-decimate to `trigCapturePts = 20 000` per signal for the wire —
a 60 s window at 1 MSps is ~960 MB raw per signal and would be dropped by
the send path anyway.
The double buffer is published (`capture.publish`) only once the frame is known
good, so a shot that yielded nothing leaves the previous capture on screen.
Dropped frames (client send-queue full) are logged.
`triggerTick` (every push tick) drives the whole FSM: re-tune rings → open
pending history files → refresh the fill measurement → due capture (send + mark
triggered + `hist.captureRange`) or due rearm → broadcast state only when it
changed (`stateUnsent`).
### 6.7 WS commands
| message | effect |
|---|---|
| `setTrigger {signal, edge, threshold, windowSec, prePercent, mode, holdoffSec}` | replace config |
| `arm` / `rearm` | explicit arm (discards pending edge) |
| `disarm` | → idle |
| `trigStop {stopped}` | pause/resume auto-rearm |
| `forceTrigger` | fire now |
Every command also refreshes the buffer-fill measurement synchronously — at
1 MSps the ring crosses the fill threshold many times inside one 33 ms tick, so
waiting for the next tick would fire on a stale measurement.
---
## 7. Browser-side trigger (`static/app.js`)
### 7.1 State handling
`onTriggerState(msg)` tracks the FSM broadcast:
- **armed** — shows `bufferFill %` while the hub is holding off on the fill
gate, so a trigger that is not yet fireable does not look broken;
- **collecting** — clears the previous snapshot, latches `trigTime` (and
`preSec`/`postSec` if the hub sent them), and lets live data sweep into the
trigger axis (see 7.3);
- **triggered / idle** — bookkeeping for the Rearm/Stop buttons.
### 7.2 Capture handling
`onTriggerCapture` parses the v2 frame into `trig.snapshot[key] = {t, v}` plus
the latched `_preS`/`_postS`. It is **ignored when the client did not enable
the trigger** (`trig.enabled`), because the hub keeps an armed trigger across
client sessions and applying a foreign capture would clobber this client's zoom
and scales. On receipt: the horizontal zoom is dropped so the whole capture is
visible, but **vertical scales (V/div, offset) persist** — they are user
settings and must survive from shot to shot.
### 7.3 Rendering modes (`buildUPlotData`)
| state | renderer | source |
|---|---|---|
| collecting, no snapshot yet | `buildTrigFillData` | live buffers, drawn on the *final* trigger axis (relative seconds, `[-pre, +post]`) so the trace sweeps in from the left |
| armed, not fired | freeze last frame | — |
| snapshot present | `buildTrigData` | the capture (or a hi-res zoom reply that covers ≥98 % of the view, else the snapshot) |
| otherwise | `buildLiveData` | rolling window |
`buildTrigData` converts to trigger-relative time (`t − trigT`), picks the
master by rate/count, decimates (cached per range+source-tag), resamples the
other traces, and normalises Y.
### 7.4 Threshold in calibrated units
The trigger threshold is held in **calibrated units** (what the user sees on
the Y axis). `sendTrigConfig()` inverts it through the signal's calibration
before sending: `raw = (calibrated − offset)/scale`, so the hub's raw
comparator fires exactly when `GAIN·signal + OFFSET` crosses the threshold.
The threshold line (`drawTriggerMarker`) maps the same calibrated threshold
through the signal's vscale: `y_norm = (threshold − offset)/div`.
---
## 8. Key constants
| constant | value | file |
|---|---|---|
| push rate | 30 Hz | `hub.go` |
| `maxPushPoints` (live) | 50 pts/signal/tick | `hub.go` |
| `trigCapturePts` (capture) | 20 000 pts/signal | `trigger.go` |
| `captureMarginSec` | 0.15 s | `trigger.go` |
| `captureStallSec` | 2.0 s | `trigger.go` |
| `autoRearmDelaySec` (default holdoff) | 0.2 s | `trigger.go` |
| `maxTriggerWindowSec` | 600 s | `trigger.go` |
| `ringBudget` default | 10 000 000 pts/signal | `hub.go` |
| `ringCapInitial` | 250 000 | `hub.go` |
| `ringCapScalar` | 100 000 | `hub.go` |
| `ringHeadroom` | 1.25 | `ringbuf.go` |
| `defaultLiveWindowSec` | 10 s | `ringbuf.go` |
| `captureLagSec` | 0.15 + 1/30 ≈ 0.183 s | `ringbuf.go` |
| `monotonicTolerance` | 5 ms | `hub.go` |
| `monotonicEMAAlpha` | 0.01 | `hub.go` |
| `MAX_CAP` (browser) | 2 000 000 pts/signal | `app.js` |
| `DEFAULT_CAP` / `TEMPORAL_CAP` | 100 000 / 500 000 | `app.js` |
| `DECIM_MIN` | 200 | `app.js` |
---
## 9. The C++ StreamHub mirror
The Go hub and the C++ StreamHub implement the same WS contracts and must stay
in sync (`AGENTS.md`): same `triggerState` FSM strings, same v2 capture frame,
same command set (`setTrigger` including `holdoffSec`, `arm`, `disarm`,
`trigStop`, `forceTrigger`), same `trigCapturePts`/`kTrigCapturePts` cap, and
the same ring/history windowing intent (`Source/Applications/StreamHub/`). A
protocol change on one side must be mirrored on the other.
+1 -11
View File
@@ -1,22 +1,12 @@
module udpstreamer-webui module udpstreamer-webui
go 1.24.9 go 1.21
require marte2/common v0.0.0 require marte2/common v0.0.0
require ( require (
github.com/andybalholm/brotli v1.1.1 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/websocket v1.5.1 // indirect github.com/gorilla/websocket v1.5.1 // indirect
github.com/klauspost/compress v1.17.9 // indirect
github.com/parquet-go/bitpack v1.0.0 // indirect
github.com/parquet-go/jsonlite v1.0.0 // indirect
github.com/parquet-go/parquet-go v0.32.0 // indirect
github.com/pierrec/lz4/v4 v4.1.21 // indirect
github.com/twpayne/go-geom v1.6.1 // indirect
golang.org/x/net v0.17.0 // indirect golang.org/x/net v0.17.0 // indirect
golang.org/x/sys v0.38.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect
) )
replace marte2/common => ../../Common/Client/go replace marte2/common => ../../Common/Client/go
-34
View File
@@ -1,38 +1,4 @@
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
github.com/alecthomas/assert/v2 v2.10.0 h1:jjRCHsj6hBJhkmhznrCzoNpbA3zqy0fYiUcYZP/GkPY=
github.com/alecthomas/assert/v2 v2.10.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/parquet-go/bitpack v1.0.0 h1:AUqzlKzPPXf2bCdjfj4sTeacrUwsT7NlcYDMUQxPcQA=
github.com/parquet-go/bitpack v1.0.0/go.mod h1:XnVk9TH+O40eOOmvpAVZ7K2ocQFrQwysLMnc6M/8lgs=
github.com/parquet-go/jsonlite v1.0.0 h1:87QNdi56wOfsE5bdgas0vRzHPxfJgzrXGml1zZdd7VU=
github.com/parquet-go/jsonlite v1.0.0/go.mod h1:nDjpkpL4EOtqs6NQugUsi0Rleq9sW/OtC1NnZEnxzF0=
github.com/parquet-go/parquet-go v0.32.0 h1:NWDqTUHfrCS4cJP/Fj2HlxvqsrVedWG3sayMkf+znzM=
github.com/parquet-go/parquet-go v0.32.0/go.mod h1:navtkAYr2LGoJVp141oXPlO/sxLvaOe3la2JEoD8+rg=
github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=
github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/twpayne/go-geom v1.6.1 h1:iLE+Opv0Ihm/ABIcvQFGIiFBXd76oBIar9drAwHFhR4=
github.com/twpayne/go-geom v1.6.1/go.mod h1:Kr+Nly6BswFsKM5sd31YaoWS5PeDDH2NftJTK7Gd028=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
-1
View File
@@ -94,7 +94,6 @@ func main() {
http.Handle("/", http.FileServer(http.FS(sub))) http.Handle("/", http.FileServer(http.FS(sub)))
http.HandleFunc("/ws", hub.HandleWebSocket) http.HandleFunc("/ws", hub.HandleWebSocket)
http.HandleFunc("/api/zoom", hub.HandleZoom) http.HandleFunc("/api/zoom", hub.HandleZoom)
http.HandleFunc("/api/export", hub.HandleExport)
http.HandleFunc("/version", func(w http.ResponseWriter, r *http.Request) { http.HandleFunc("/version", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, buildVersion) fmt.Fprint(w, buildVersion)
}) })
+120 -590
View File
@@ -476,24 +476,14 @@ let cursorsDirty = false; // if true, redraw all plots to update cursor lines
// Rolling-window anchor used to keep cursors visually fixed while live data scrolls. // Rolling-window anchor used to keep cursors visually fixed while live data scrolls.
let _cursorAnchorNow = null; let _cursorAnchorNow = null;
// Horizontal value rulers. The on/off toggle is global, but each plot keeps its // Horizontal value rulers — stored in normalized division units (the shared
// own pair of normalized-division positions (rulerState), so dragging Y1 in // y scale, -4.5…4.5) so one pair applies to every plot regardless of V/div.
// one plot does not move it in the others. const rulers = { mode: 'off', yA: null, yB: null };
const rulers = { mode: 'off', plotId: null };
const rulerState = {}; // plotId → { yA, yB }
function getRulerState(plotId) { // Layout — [label, cssClass, cols, rows]
if (!rulerState[plotId]) rulerState[plotId] = { yA: null, yB: null };
return rulerState[plotId];
}
// Layout — [label, cssClass, cols, rows, (optional) plotCount].
// Custom (non-uniform) layouts carry an explicit plotCount; the grid template
// and the spanning cells are defined in style.css under #plot-grid.<class>.
const LAYOUTS = [ const LAYOUTS = [
['1×1', 'l1x1', 1, 1], ['1×2', 'l1x2', 1, 2], ['2×1', 'l2x1', 2, 1], ['1×3', 'l1x3', 1, 3], ['1×1', 'l1x1', 1, 1], ['1×2', 'l1x2', 1, 2], ['2×1', 'l2x1', 2, 1], ['1×3', 'l1x3', 1, 3],
['3×1', 'l3x1', 3, 1], ['2×2', 'l2x2', 2, 2], ['1×4', 'l1x4', 1, 4], ['4×1', 'l4x1', 4, 1], ['3×1', 'l3x1', 3, 1], ['2×2', 'l2x2', 2, 2], ['1×4', 'l1x4', 1, 4], ['4×1', 'l4x1', 4, 1],
['1+2', 'l1p2', 2, 2, 3], // one plot spanning the top row, two below
]; ];
let currentLayout = 'l1x1'; let currentLayout = 'l1x1';
let colFrs = [1]; // fractional column sizes (sum = cols) let colFrs = [1]; // fractional column sizes (sum = cols)
@@ -826,7 +816,6 @@ function onConfig(msg) {
} }
buildSidebar(); buildSidebar();
buildTrigSignalSelect(); buildTrigSignalSelect();
maybeRestoreViewLate();
} }
/* ════════════════════════════════════════════════════════════════ /* ════════════════════════════════════════════════════════════════
@@ -936,21 +925,12 @@ function wsSend(obj) {
function sendWindow() { function sendWindow() {
wsSend({ type: 'setWindow', seconds: windowSec }); wsSend({ type: 'setWindow', seconds: windowSec });
} }
// trig.threshold is held in calibrated units. The hub.s comparator runs on raw // trig.threshold is held in calibrated units. The hub's comparator runs on raw
// samples, so invert on the way out: raw = (calibrated - offset) / scale. // samples, so invert on the way out: raw = (calibrated - offset) / scale.
function sendTrigConfig() { function sendTrigConfig() {
const cal = trig.signal ? calForKey(trig.signal) : Calib.IDENTITY; const cal = trig.signal ? calForKey(trig.signal) : Calib.IDENTITY;
// A negative calibration gain flips the signal on screen (v_cal = v_raw·scale
// + offset with scale < 0), so a calibrated rising edge is a raw FALLING
// edge. The hub compares raw samples, so send the raw direction that matches
// the edge the user picked on the calibrated trace.
let edge = trig.edge;
if (cal.scale < 0) {
if (edge === 'rising') edge = 'falling';
else if (edge === 'falling') edge = 'rising';
}
wsSend({ wsSend({
type: 'setTrigger', signal: trig.signal, edge: edge, type: 'setTrigger', signal: trig.signal, edge: trig.edge,
threshold: Calib.invertCal(trig.threshold, cal), windowSec: trig.windowSec, threshold: Calib.invertCal(trig.threshold, cal), windowSec: trig.windowSec,
prePercent: trig.prePercent, mode: trig.mode, holdoffSec: trig.holdoffSec, prePercent: trig.prePercent, mode: trig.mode, holdoffSec: trig.holdoffSec,
}); });
@@ -1352,11 +1332,7 @@ function decimateAsync(cacheKey, t, v, threshold, gen) {
return result; return result;
} }
} }
// Never hand out a stale decimation: drawing it (at its old timestamps) and return cached || null; // stale entry, or nothing to draw yet
// then the fresh one a frame later is what makes the trace jump/shimmer on
// every push. Return null instead — the caller holds the previous render
// until the worker's fresh result lands (it flags the plot for redraw).
return null;
} }
// Evict stale decimation cache entries for a plot (call when zoom range changes). // Evict stale decimation cache entries for a plot (call when zoom range changes).
@@ -1862,9 +1838,15 @@ function drawCursorLines(u, p) {
if (vNorm === null) return; if (vNorm === null) return;
const cy = u.valToPos(vNorm, 'y', true); const cy = u.valToPos(vNorm, 'y', true);
if (cy < bbox.top || cy > bbox.top + bbox.height) return; if (cy < bbox.top || cy > bbox.top + bbox.height) return;
// Calibrated value at the cursor time, from the raw source (matches // Un-transform normalized value back to real units for display
// the hover and the cursor readouts in every display mode). // y_norm = (y_raw - offset) / divValue → y_raw = y_norm * divValue + offset
const vReal = calibratedValueAt(key, val); const vs = sigVScale[vsKeyFor(p.id, key)];
let vReal = vNorm;
if (vs) {
const dv = vs._resolvedDiv || vs.divValue || 1;
const ofs = vs._resolvedOffset != null ? vs._resolvedOffset : (vs.offset || 0);
vReal = vNorm * dv + ofs;
}
const tc = getSigStyle(key).color; const tc = getSigStyle(key).color;
// Diamond marker at intersection // Diamond marker at intersection
ctx.fillStyle = tc; ctx.fillStyle = tc;
@@ -1878,7 +1860,7 @@ function drawCursorLines(u, p) {
ctx.closePath(); ctx.closePath();
ctx.fill(); ctx.fill();
// Value text next to diamond (real units) // Value text next to diamond (real units)
const str = vReal === null ? '—' : (Math.abs(vReal) >= 10000 ? vReal.toExponential(2) : parseFloat(vReal.toPrecision(4)).toString()); const str = Math.abs(vReal) >= 10000 ? vReal.toExponential(2) : parseFloat(vReal.toPrecision(4)).toString();
ctx.fillStyle = tc; ctx.fillStyle = tc;
ctx.font = '11px monospace'; ctx.font = '11px monospace';
const currentAlign = ctx.textAlign; const currentAlign = ctx.textAlign;
@@ -1916,8 +1898,6 @@ function rulerRawValue(p, yNorm) {
// Draw the horizontal value rulers (called from the draw hook). // Draw the horizontal value rulers (called from the draw hook).
function drawRulerLines(u, p) { function drawRulerLines(u, p) {
if (rulers.mode !== 'on') return; if (rulers.mode !== 'on') return;
const rs = rulerState[p.id];
if (!rs) return;
const { ctx, bbox } = u; const { ctx, bbox } = u;
if (!bbox) return; if (!bbox) return;
@@ -1946,8 +1926,8 @@ function drawRulerLines(u, p) {
ctx.restore(); ctx.restore();
}; };
drawLine(rs.yA, 'rgba(166,227,161,0.85)', 'Y1'); drawLine(rulers.yA, 'rgba(166,227,161,0.85)', 'Y1');
drawLine(rs.yB, 'rgba(243,139,168,0.85)', 'Y2'); drawLine(rulers.yB, 'rgba(243,139,168,0.85)', 'Y2');
} }
// Compute the rolling-window anchor ("newest common timestamp") for a plot. // Compute the rolling-window anchor ("newest common timestamp") for a plot.
@@ -2153,9 +2133,8 @@ function createUPlot(p) {
const rect = p.uplot.over.getBoundingClientRect(); const rect = p.uplot.over.getBoundingClientRect();
const { min, max } = p.uplot.scales.y; const { min, max } = p.uplot.scales.y;
const toY = val => rect.top + (1 - (val - min) / (max - min)) * rect.height; const toY = val => rect.top + (1 - (val - min) / (max - min)) * rect.height;
const rs = rulerState[p.id]; if (rulers.yA !== null && Math.abs(clientY - toY(rulers.yA)) <= CURSOR_SNAP_PX) return 'A';
if (rs && rs.yA !== null && Math.abs(clientY - toY(rs.yA)) <= CURSOR_SNAP_PX) return 'A'; if (rulers.yB !== null && Math.abs(clientY - toY(rulers.yB)) <= CURSOR_SNAP_PX) return 'B';
if (rs && rs.yB !== null && Math.abs(clientY - toY(rs.yB)) <= CURSOR_SNAP_PX) return 'B';
return null; return null;
} }
@@ -2192,10 +2171,8 @@ function createUPlot(p) {
// Set cursor position immediately on mousedown // Set cursor position immediately on mousedown
if (yTarget) { if (yTarget) {
rulers.plotId = p.id; // the readout follows the plot whose rulers moved if (yTarget === 'A') rulers.yA = _rulerValFromEvent(e);
const rs = getRulerState(p.id); else rulers.yB = _rulerValFromEvent(e);
if (yTarget === 'A') rs.yA = _rulerValFromEvent(e);
else rs.yB = _rulerValFromEvent(e);
} else if (target === 'A') cursors.tA = _cursorValFromEvent(e); } else if (target === 'A') cursors.tA = _cursorValFromEvent(e);
else cursors.tB = _cursorValFromEvent(e); else cursors.tB = _cursorValFromEvent(e);
updateCursorReadout(); updateCursorReadout();
@@ -2203,9 +2180,8 @@ function createUPlot(p) {
const onMove = ev => { const onMove = ev => {
if (yTarget) { if (yTarget) {
const rs = getRulerState(p.id); if (yTarget === 'A') rulers.yA = _rulerValFromEvent(ev);
if (yTarget === 'A') rs.yA = _rulerValFromEvent(ev); else rulers.yB = _rulerValFromEvent(ev);
else rs.yB = _rulerValFromEvent(ev);
} else if (target === 'A') cursors.tA = _cursorValFromEvent(ev); } else if (target === 'A') cursors.tA = _cursorValFromEvent(ev);
else cursors.tB = _cursorValFromEvent(ev); else cursors.tB = _cursorValFromEvent(ev);
updateCursorReadout(); updateCursorReadout();
@@ -2471,12 +2447,8 @@ function buildLiveData(p) {
let dec; let dec;
if (cached) { if (cached) {
dec = cached; dec = cached;
} else if (p.uplot && p.uplot.data && p.uplot.data[0] && p.uplot.data[0].length) {
// Fresh decimation not ready yet — hold the previous render so the trace
// does not flicker between a stale decimation and the fresh one.
return p.uplot.data;
} else { } else {
// First render: worker job submitted, nothing on screen yet — sync. // Worker job submitted — sync fallback this frame so the plot isn't blank.
dec = decimate(masterRaw.t, masterRaw.v, targetPts); dec = decimate(masterRaw.t, masterRaw.v, targetPts);
} }
sharedT = dec.t; sharedT = dec.t;
@@ -2549,14 +2521,7 @@ function buildTrigData(p) {
// same-length snapshot slice for the same range, so it is tagged separately. // same-length snapshot slice for the same range, so it is tagged separately.
const cacheKey = `${p.id}:${masterKey}:${t0.toFixed(6)}:${t1.toFixed(6)}:${masterRaw.t.length}:${usedFetched ? 'hi' : 'snap'}`; const cacheKey = `${p.id}:${masterKey}:${t0.toFixed(6)}:${t1.toFixed(6)}:${masterRaw.t.length}:${usedFetched ? 'hi' : 'snap'}`;
const cachedDec = decimateAsync(cacheKey, masterRaw.t, masterRaw.v, targetPts); const cachedDec = decimateAsync(cacheKey, masterRaw.t, masterRaw.v, targetPts);
let dec; const dec = cachedDec || decimate(masterRaw.t, masterRaw.v, targetPts);
if (cachedDec) {
dec = cachedDec;
} else if (p.uplot && p.uplot.data && p.uplot.data[0] && p.uplot.data[0].length) {
return p.uplot.data; // hold the previous render until the fresh decimation lands
} else {
dec = decimate(masterRaw.t, masterRaw.v, targetPts);
}
// Convert absolute → relative seconds // Convert absolute → relative seconds
const sharedT = new Float64Array(dec.t.length); const sharedT = new Float64Array(dec.t.length);
for (let i = 0; i < dec.t.length; i++) sharedT[i] = dec.t[i] - trigT; for (let i = 0; i < dec.t.length; i++) sharedT[i] = dec.t[i] - trigT;
@@ -2613,15 +2578,8 @@ function buildTrigFillData(p) {
masterV = masterRaw.v; masterV = masterRaw.v;
} else { } else {
const cacheKey = `${p.id}:${masterKey}:trigfill`; const cacheKey = `${p.id}:${masterKey}:trigfill`;
const decd = decimateAsync(cacheKey, masterRaw.t, masterRaw.v, targetPts, _dataGen); const dec = decimateAsync(cacheKey, masterRaw.t, masterRaw.v, targetPts, _dataGen) ||
let dec; decimate(masterRaw.t, masterRaw.v, targetPts);
if (decd) {
dec = decd;
} else if (p.uplot && p.uplot.data && p.uplot.data[0] && p.uplot.data[0].length) {
return p.uplot.data; // hold until the fresh decimation is ready
} else {
dec = decimate(masterRaw.t, masterRaw.v, targetPts);
}
sharedAbsT = dec.t; sharedAbsT = dec.t;
masterV = dec.v; masterV = dec.v;
} }
@@ -2795,19 +2753,7 @@ function updateCursorBtnVisibility() {
under one — so a zoom, a pan or a new capture can leave them outside the under one — so a zoom, a pan or a new capture can leave them outside the
viewport entirely, with no way to get them back: they are dragged by grabbing viewport entirely, with no way to get them back: they are dragged by grabbing
their line, and an off-screen line cannot be grabbed. */ their line, and an off-screen line cannot be grabbed. */
function resetRulers() {
// Re-place every plot's rulers at the default ±2 divisions, like
// resetCursors re-places the vertical cursors.
plots.forEach(p => {
const rs = getRulerState(p.id);
rs.yA = -2; rs.yB = 2;
});
updateCursorReadout();
cursorsDirty = true;
}
function resetCursors() { function resetCursors() {
resetRulers();
const refPlot = plots.find(p => p.uplot); const refPlot = plots.find(p => p.uplot);
if (!refPlot) return; if (!refPlot) return;
const { min, max } = refPlot.uplot.scales.x; const { min, max } = refPlot.uplot.scales.x;
@@ -2844,13 +2790,9 @@ document.getElementById('btn-ruler').addEventListener('click', () => {
rulers.mode = rulers.mode === 'off' ? 'on' : 'off'; rulers.mode = rulers.mode === 'off' ? 'on' : 'off';
const btn = document.getElementById('btn-ruler'); const btn = document.getElementById('btn-ruler');
btn.classList.toggle('active', rulers.mode === 'on'); btn.classList.toggle('active', rulers.mode === 'on');
if (rulers.mode === 'on') { if (rulers.mode === 'on' && rulers.yA === null && rulers.yB === null) {
// Auto-place every plot at ±2 divisions from the centre on first use; // Auto-place at ±2 divisions from the centre on first use.
// afterwards each plot keeps its own positions. rulers.yA = -2; rulers.yB = 2;
plots.forEach(pl => {
const rs = getRulerState(pl.id);
if (rs.yA === null && rs.yB === null) { rs.yA = -2; rs.yB = 2; }
});
} }
updateCursorReadout(); updateCursorReadout();
cursorsDirty = true; cursorsDirty = true;
@@ -2867,9 +2809,16 @@ function getValueAtCursor(p, t) {
if (!p.uplot || t === null) return null; if (!p.uplot || t === null) return null;
const key = plotActiveSignal[p.id] || (p.traces.length === 1 ? p.traces[0] : null); const key = plotActiveSignal[p.id] || (p.traces.length === 1 ? p.traces[0] : null);
if (!key) return null; if (!key) return null;
// Interpolate the raw wire value and apply the calibration explicitly, so const idx = p.traces.indexOf(key);
// cursor readouts match the hover in every display mode. if (idx < 0) return null;
return calibratedValueAt(key, t); const vNorm = interpAtTime(p.uplot, idx + 1, t);
if (vNorm === null) return null;
// Un-normalize: y_norm = (y_raw - offset) / divValue
const vs = sigVScale[p.id + ':' + key];
if (!vs) return vNorm;
const dv = vs._resolvedDiv != null ? vs._resolvedDiv : (vs.divValue || 1);
const ofs = vs._resolvedOffset != null ? vs._resolvedOffset : (vs.offset || 0);
return vNorm * dv + ofs;
} }
// Update per-plot cursor value readouts (A, B, ΔV) for all plots. // Update per-plot cursor value readouts (A, B, ΔV) for all plots.
@@ -2902,64 +2851,6 @@ function rawFromNorm(p, key, vNorm) {
const ofs = vs._resolvedOffset != null ? vs._resolvedOffset : (vs.offset || 0); const ofs = vs._resolvedOffset != null ? vs._resolvedOffset : (vs.offset || 0);
return vNorm * dv + ofs; return vNorm * dv + ofs;
} }
// Linear interpolation of a sorted (t, v) pair at absolute time tAbs. Returns
// null outside the data's range — never fabricated, so an export or readout
// cannot invent samples the signal never had.
function interpSortedRaw(t, v, tAbs) {
if (!t || t.length === 0) return null;
if (tAbs < t[0] || tAbs > t[t.length - 1]) return null;
let lo = 0, hi = t.length - 1;
while (lo < hi) { const m = (lo + hi) >> 1; if (t[m] < tAbs) lo = m + 1; else hi = m; }
if (lo === 0) return v[0] ?? null;
const t0 = t[lo - 1], t1 = t[lo];
const v0 = v[lo - 1], v1 = v[lo];
if (v0 == null || v1 == null) return v0 ?? v1 ?? null;
return v0 + (tAbs - t0) / (t1 - t0) * (v1 - v0);
}
// Binary-search linear interpolation of a circular buffer at time t.
function interpCircular(buf, t) {
if (!buf || buf.size === 0) return null;
const { cap, size, head } = buf;
const start = (size === cap) ? head : 0;
const physAt = k => (start + k) % cap;
let lo = 0, hi = size;
while (lo < hi) { const m = (lo + hi) >> 1; if (buf.t[physAt(m)] < t) lo = m + 1; else hi = m; }
if (lo === 0) return buf.v[physAt(0)] ?? null;
if (lo >= size) return buf.v[physAt(size - 1)] ?? null;
const t0 = buf.t[physAt(lo - 1)], t1 = buf.t[physAt(lo)];
const v0 = buf.v[physAt(lo - 1)], v1 = buf.v[physAt(lo)];
if (v0 == null || v1 == null) return v0 ?? v1 ?? null;
return v0 + (t - t0) / (t1 - t0) * (v1 - v0);
}
// Raw (uncalibrated) value of `key` at absolute time tAbs, from the best
// available raw source: trigger snapshot → fetched zoom data → live push
// buffer. All three store wire values, so calibration is applied here, at the
// point of display, exactly once.
function rawAtAbsTime(key, tAbs) {
if (trig.snapshot) {
const s = trig.snapshot[key];
if (s && s.t.length) { const v = interpSortedRaw(s.t, s.v, tAbs); if (v != null) return v; }
}
for (const p of plots) {
const zd = zoomData[p.id];
if (!zd) continue;
const s = zd.signals[key];
if (s && s.t.length) { const v = interpSortedRaw(s.t, s.v, tAbs); if (v != null) return v; }
}
const buf = buffers[key];
if (buf && buf.size) { const v = interpCircular(buf, tAbs); if (v != null) return v; }
return null;
}
// Calibrated value of `key` at axis time t. Under a trigger the axis is
// relative to the trigger instant, so convert to absolute first.
function calibratedValueAt(key, t) {
const tAbs = (inTrigWindow() && trig.trigTime != null) ? trig.trigTime + t : t;
const raw = rawAtAbsTime(key, tAbs);
return raw === null ? null : Calib.applyCal(raw, calForKey(key));
}
function hideHoverReadout() { function hideHoverReadout() {
document.getElementById('hover-readout').style.display = 'none'; document.getElementById('hover-readout').style.display = 'none';
@@ -2979,14 +2870,13 @@ function showHoverReadout(p, e) {
const tStr = inTrigWindow() ? fmtDuration(t, span, true) : fmtLiveTime(t, span); const tStr = inTrigWindow() ? fmtDuration(t, span, true) : fmtLiveTime(t, span);
let html = '<div class="hov-time">' + escHtml(tStr) + '</div>'; let html = '<div class="hov-time">' + escHtml(tStr) + '</div>';
p.traces.forEach((key, idx) => { p.traces.forEach((key, idx) => {
const vNorm = interpAtTime(p.uplot, idx + 1, t);
const name = key.includes(':') ? key.slice(key.indexOf(':') + 1) : key; const name = key.includes(':') ? key.slice(key.indexOf(':') + 1) : key;
// rawFromNorm inverts the vscale transform, which Task 7 made operate on
// calibrated values — so this is already in calibrated units.
const unit = unitForKey(key); const unit = unitForKey(key);
// Interpolate the raw wire value and apply the calibration explicitly, const val = vNorm === null ? '—'
// so the hover is correct in every display mode (analog, digital, : (_fmtVal(rawFromNorm(p, key, vNorm)) + (unit ? ' ' + unit : ''));
// mixed) and independent of the vscale state.
const vCal = calibratedValueAt(key, t);
const val = vCal === null ? '—'
: (_fmtVal(vCal) + (unit ? ' ' + unit : ''));
html += '<div class="hov-row"><span class="hov-dot" style="background:' + html += '<div class="hov-row"><span class="hov-dot" style="background:' +
escHtml(getSigStyle(key).color) + '"></span>' + escHtml(getSigStyle(key).color) + '"></span>' +
'<span class="hov-name">' + escHtml(name) + '</span>' + '<span class="hov-name">' + escHtml(name) + '</span>' +
@@ -3004,25 +2894,17 @@ function showHoverReadout(p, e) {
el.style.top = Math.max(4, y) + 'px'; el.style.top = Math.max(4, y) + 'px';
} }
// Update the Y1/Y2/ΔY ruler readout, expressed in the raw units of the plot // Update the Y1/Y2/ΔY ruler readout, expressed in the raw units of the first
// whose rulers were last moved, falling back to the first plot with a signal. // plot that has an active (or sole) signal.
function updateRulerReadout() { function updateRulerReadout() {
const box = document.getElementById('ruler-readout'); const box = document.getElementById('ruler-readout');
const on = rulers.mode === 'on'; const on = rulers.mode === 'on';
box.style.display = on ? '' : 'none'; box.style.display = on ? '' : 'none';
if (!on) return; if (!on) return;
let ref = null; const ref = plots.find(p => p.uplot && p.traces.length > 0 &&
if (rulers.plotId !== null) { rulerRawValue(p, 0) !== null);
const pl = plots.find(p => p.id === rulers.plotId); const conv = y => (y === null || !ref) ? null : rulerRawValue(ref, y);
if (pl && pl.uplot && pl.traces.length > 0) ref = pl; const vA = conv(rulers.yA), vB = conv(rulers.yB);
}
if (!ref) {
ref = plots.find(p => p.uplot && p.traces.length > 0 &&
rulerRawValue(p, 0) !== null) || null;
}
const rs = ref ? rulerState[ref.id] : null;
const conv = y => (y === null || !ref || !rs) ? null : rulerRawValue(ref, y);
const vA = conv(rs ? rs.yA : null), vB = conv(rs ? rs.yB : null);
document.getElementById('cur-y1').textContent = 'Y1: ' + fmtVal(vA); document.getElementById('cur-y1').textContent = 'Y1: ' + fmtVal(vA);
document.getElementById('cur-y2').textContent = 'Y2: ' + fmtVal(vB); document.getElementById('cur-y2').textContent = 'Y2: ' + fmtVal(vB);
document.getElementById('cur-dy').textContent = document.getElementById('cur-dy').textContent =
@@ -3473,42 +3355,23 @@ function initPlotCfgBar(plotId, p) {
/* ════════════════════════════════════════════════════════════════ /* ════════════════════════════════════════════════════════════════
Layout management Layout management
════════════════════════════════════════════════════════════════ */ ════════════════════════════════════════════════════════════════ */
// Returns the number of plot cells in a layout. Custom layouts carry an // Returns the number of plot cells in a layout (cols × rows).
// explicit plotCount; uniform ones are cols × rows.
function layoutPlotCount(cls) { function layoutPlotCount(cls) {
const entry = LAYOUTS.find(l => l[1] === cls);
if (entry) {
if (entry.length >= 5) return entry[4];
return entry[2] * entry[3];
}
const m = cls.match(/^l(\d+)x(\d+)$/); const m = cls.match(/^l(\d+)x(\d+)$/);
return m ? parseInt(m[1]) * parseInt(m[2]) : 1; return m ? parseInt(m[1]) * parseInt(m[2]) : 1;
} }
// Build a small SVG grid thumbnail for a layout entry. Custom (non-uniform) // Build a small SVG grid thumbnail for a given cols×rows layout.
// layouts draw their own cell arrangement. function layoutSVG(cols, rows) {
function layoutSVG(entry) {
const W = 28, H = 20, GAP = 1.5, PAD = 1.5; const W = 28, H = 20, GAP = 1.5, PAD = 1.5;
const cw = (W - PAD * 2 - GAP * (cols - 1)) / cols;
const ch = (H - PAD * 2 - GAP * (rows - 1)) / rows;
let rects = ''; let rects = '';
if (entry[1] === 'l1p2') { for (let r = 0; r < rows; r++) {
// 1+2: one full-width cell on top, two side by side below. for (let c = 0; c < cols; c++) {
const cw = (W - PAD * 2 - GAP) / 2; const x = (PAD + c * (cw + GAP)).toFixed(1);
const ch = (H - PAD * 2 - GAP) / 2; const y = (PAD + r * (ch + GAP)).toFixed(1);
const y2 = (PAD + ch + GAP).toFixed(1); rects += `<rect x="${x}" y="${y}" width="${cw.toFixed(1)}" height="${ch.toFixed(1)}" rx="1.5"/>`;
const x2 = (PAD + cw + GAP).toFixed(1);
rects += `<rect x="${PAD}" y="${PAD}" width="${(W - PAD * 2).toFixed(1)}" height="${ch.toFixed(1)}" rx="1.5"/>`;
rects += `<rect x="${PAD}" y="${y2}" width="${cw.toFixed(1)}" height="${ch.toFixed(1)}" rx="1.5"/>`;
rects += `<rect x="${x2}" y="${y2}" width="${cw.toFixed(1)}" height="${ch.toFixed(1)}" rx="1.5"/>`;
} else {
const [, , cols, rows] = entry;
const cw = (W - PAD * 2 - GAP * (cols - 1)) / cols;
const ch = (H - PAD * 2 - GAP * (rows - 1)) / rows;
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
const x = (PAD + c * (cw + GAP)).toFixed(1);
const y = (PAD + r * (ch + GAP)).toFixed(1);
rects += `<rect x="${x}" y="${y}" width="${cw.toFixed(1)}" height="${ch.toFixed(1)}" rx="1.5"/>`;
}
} }
} }
return `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}">` return `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}">`
@@ -3538,7 +3401,7 @@ function applyLayout(cls) {
// Update button label // Update button label
const btn = document.getElementById('btn-layout'); const btn = document.getElementById('btn-layout');
if (btn) btn.innerHTML = layoutSVG(entry) + ' <span>' + label + '</span> ▾'; if (btn) btn.innerHTML = layoutSVG(cols, rows) + ' <span>' + label + '</span> ▾';
// Update active state in menu // Update active state in menu
document.querySelectorAll('.layout-menu-item') document.querySelectorAll('.layout-menu-item')
@@ -3573,12 +3436,11 @@ function applyLayout(cls) {
function buildLayoutMenu() { function buildLayoutMenu() {
const menu = document.getElementById('layout-menu'); const menu = document.getElementById('layout-menu');
LAYOUTS.forEach(entry => { LAYOUTS.forEach(([label, cls, cols, rows]) => {
const [label, cls] = entry;
const item = document.createElement('button'); const item = document.createElement('button');
item.className = 'layout-menu-item' + (cls === currentLayout ? ' active' : ''); item.className = 'layout-menu-item' + (cls === currentLayout ? ' active' : '');
item.dataset.layout = cls; item.dataset.layout = cls;
item.innerHTML = layoutSVG(entry) + '<span>' + label + '</span>'; item.innerHTML = layoutSVG(cols, rows) + '<span>' + label + '</span>';
item.addEventListener('click', () => { item.addEventListener('click', () => {
applyLayout(cls); applyLayout(cls);
menu.classList.remove('open'); menu.classList.remove('open');
@@ -3605,20 +3467,9 @@ function buildLayoutMenu() {
/* ════════════════════════════════════════════════════════════════ /* ════════════════════════════════════════════════════════════════
Export CSV (all plots) — fetches full-resolution data from ring Export CSV (all plots) — fetches full-resolution data from ring
════════════════════════════════════════════════════════════════ */ ════════════════════════════════════════════════════════════════ */
// Shared busy state for the export dropdown: prevents re-entry and shows
// progress on the selector while a (possibly large) export runs.
let exportBusy = false;
function setExportBusy(busy) {
exportBusy = busy;
const sel = document.getElementById('export-select');
if (!sel) return;
sel.disabled = busy;
const ph = sel.querySelector('option[value=""]');
if (ph) ph.textContent = busy ? '\u23f3 Exporting\u2026' : '\u23ea Export';
}
async function exportAllCSV() { async function exportAllCSV() {
if (exportBusy) return; const btn = document.getElementById('btn-csv-all');
if (btn.disabled) return;
const inTrigMode = trig.enabled && trig.snapshot !== null; const inTrigMode = trig.enabled && trig.snapshot !== null;
@@ -3631,8 +3482,8 @@ async function exportAllCSV() {
let t0, t1, relOffset = 0; let t0, t1, relOffset = 0;
if (inTrigMode) { if (inTrigMode) {
// Export the full trigger window around the trigger event. // Export the full trigger window around the trigger event.
t0 = trig.trigTime - activePreSec(); t0 = trig.trigTime - trigPreSec();
t1 = trig.trigTime + activePostSec(); t1 = trig.trigTime + trigPostSec();
relOffset = trig.trigTime; relOffset = trig.trigTime;
} else { } else {
// Use the current zoom range if active, else the rolling window. // Use the current zoom range if active, else the rolling window.
@@ -3653,52 +3504,60 @@ async function exportAllCSV() {
t1 = plotNow; t1 = plotNow;
} }
} }
if (!(t1 > t0)) return;
exportBusy = true; // Show loading state.
// Cap the export. A full window at a megasample rate is hundreds of MB raw const origLabel = btn.textContent;
// (the old exact-timestamp merge exploded into millions of rows and crashed btn.textContent = '⏳ Downloading…';
// the tab); ask the hub for a min/max-decimated envelope — the same scope btn.disabled = true;
// style reduction the live view uses — and cap the number of rows.
const BUDGET = 100000; // max rows per signal
setExportBusy(true);
// Fetch full-resolution ring data (n=0 → no decimation).
let ringSignals = null; let ringSignals = null;
if (!inTrigMode) { try {
try { ringSignals = await wsZoomRequest(t0, t1, 0, keys);
ringSignals = await wsZoomRequest(t0, t1, BUDGET, keys); } catch (e) {
} catch (e) { console.warn('CSV export: ring fetch failed, falling back to push buffer', e);
console.warn('CSV export: ring fetch failed, falling back to local data', e); } finally {
} btn.textContent = origLabel;
btn.disabled = false;
} }
setExportBusy(false);
// Per-signal raw source: hub ring (whole window, decimated) → trigger // Build per-signal time/value arrays.
// snapshot (already \u226420k pts) → local push buffer. // Priority: ring buffer (full res) → trigger snapshot → push buffer.
const slices = keys.map(key => { const slices = keys.map(key => {
if (!inTrigMode) { const rd = ringSignals && ringSignals[key];
const rd = ringSignals && ringSignals[key]; if (rd && rd.t && rd.t.length > 0) {
if (rd && rd.t && rd.t.length > 0) return { key, t: rd.t, v: rd.v }; const t = rd.t, v = rd.v;
if (inTrigMode) {
return { t: Array.from(t).map(ts => ts - relOffset), v: Array.from(v) };
}
return { t: Array.from(t), v: Array.from(v) };
} }
// Fallback: push buffer or trigger snapshot.
if (inTrigMode) { if (inTrigMode) {
const raw = trig.snapshot[key] || { t: new Float64Array(0), v: new Float64Array(0) }; const raw = trig.snapshot[key] || { t: new Float64Array(0), v: new Float64Array(0) };
return { key, t: raw.t, v: raw.v }; return { t: Array.from(raw.t).map(ts => ts - relOffset), v: Array.from(raw.v) };
} }
const buf = buffers[key]; const buf = buffers[key]; if (!buf) return { t: [], v: [] };
const sl = buf ? getBufferSliceRange(buf, t0, t1) : { t: new Float64Array(0), v: new Float64Array(0) }; const sl = getBufferSliceRange(buf, t0, t1);
return { key, t: sl.t, v: sl.v }; return { t: Array.from(sl.t), v: Array.from(sl.v) };
}); });
const present = slices.filter(s => s.t.length > 0);
if (!present.length) return;
// Master time grid = the signal with the most samples; every other signal is // Merge all timestamps and build aligned rows.
// resampled onto it (linear, no extrapolation). Cells outside a signal's own const allT = new Set();
// span stay empty rather than being fabricated, so continuous signals export slices.forEach(s => s.t.forEach(t => allT.add(t)));
// without holes and no value is invented. const sortedT = Array.from(allT).sort((a, b) => a - b);
let master = present[0]; if (!sortedT.length) return;
present.forEach(s => { if (s.t.length > master.t.length) master = s; });
const cals = new Map(keys.map(k => [k, calForKey(k)])); const lookups = slices.map(s => {
const m = new Map();
s.t.forEach((t, i) => m.set(t, s.v[i]));
return m;
});
// Strip "sourceId:" prefix from column headers for readability, and append
// the effective unit. These values come straight from the ring/history/
// snapshot and never pass through applyVScaleNorm, so calibrate them here.
const cals = keys.map(k => calForKey(k));
const displayKeys = keys.map(k => { const displayKeys = keys.map(k => {
const name = k.includes(':') ? k.split(':').slice(1).join(':') : k; const name = k.includes(':') ? k.split(':').slice(1).join(':') : k;
const u = unitForKey(k); const u = unitForKey(k);
@@ -3707,21 +3566,10 @@ async function exportAllCSV() {
}); });
const timeCol = '"' + (inTrigMode ? 'time_rel_s' : 'time_s') + '"'; const timeCol = '"' + (inTrigMode ? 'time_rel_s' : 'time_s') + '"';
const hdr = [timeCol, ...displayKeys].join(','); const hdr = [timeCol, ...displayKeys].join(',');
const rows = sortedT.map(t =>
const rows = new Array(master.t.length); [t.toFixed(9), ...lookups.map((lk, i) =>
for (let i = 0; i < master.t.length; i++) { lk.has(t) ? Calib.applyCal(lk.get(t), cals[i]) : '')].join(',')
const tAbs = master.t[i]; );
const cells = present.map(s => {
if (s === master) {
return Calib.applyCal(master.v[i], cals.get(s.key));
}
const v = interpSortedRaw(s.t, s.v, tAbs);
return v === null ? '' : Calib.applyCal(v, cals.get(s.key));
});
const tt = inTrigMode ? tAbs - relOffset : tAbs;
rows[i] = [tt.toFixed(9), ...cells].join(',');
}
const blob = new Blob([hdr + '\n' + rows.join('\n')], { type: 'text/csv' }); const blob = new Blob([hdr + '\n' + rows.join('\n')], { type: 'text/csv' });
const a = document.createElement('a'); const a = document.createElement('a');
a.href = URL.createObjectURL(blob); a.href = URL.createObjectURL(blob);
@@ -3906,10 +3754,6 @@ function deletePlot(plotId) {
let _dbgTick = 0; let _dbgTick = 0;
let _dataGen = 0; // incremented each time new data arrives let _dataGen = 0; // incremented each time new data arrives
function renderDirtyPlots() { function renderDirtyPlots() {
// Schedule the next frame FIRST: an exception below must never kill the
// animation loop, or every plot would freeze until a page refresh.
requestAnimationFrame(renderDirtyPlots);
try {
// Compute global "now" once — shared by all rolling-window plots this frame. // Compute global "now" once — shared by all rolling-window plots this frame.
const globalPlotNow = getGlobalNow(); const globalPlotNow = getGlobalNow();
@@ -3979,7 +3823,7 @@ function renderDirtyPlots() {
plots.forEach(p => { plots.forEach(p => {
if (!p.needsRedraw || !p.uplot || p.traces.length === 0) return; if (!p.needsRedraw || !p.uplot || p.traces.length === 0) return;
try {
const inTrigModeNow = inTrigWindow(); const inTrigModeNow = inTrigWindow();
// The x tick formatter and the cursor-sync group are baked into the uPlot // The x tick formatter and the cursor-sync group are baked into the uPlot
// options at construction. A plot built in live mode therefore keeps // options at construction. A plot built in live mode therefore keeps
@@ -3994,10 +3838,7 @@ function renderDirtyPlots() {
if (isRolling && _dataGen === p.lastDataGen && p.uplot.data && p.uplot.data[0] && p.uplot.data[0].length > 0) { if (isRolling && _dataGen === p.lastDataGen && p.uplot.data && p.uplot.data[0] && p.uplot.data[0].length > 0) {
p.needsRedraw = false; p.needsRedraw = false;
zoomGuard = true; zoomGuard = true;
// Use the same per-plot anchor as the rebuild path, so the rolling window p.uplot.setScale('x', { min: globalPlotNow - windowSec, max: globalPlotNow });
// does not jump when the frame switches between the two.
const plotNow = computePlotNow(p);
p.uplot.setScale('x', { min: plotNow - windowSec, max: plotNow });
zoomGuard = false; zoomGuard = false;
return; return;
} }
@@ -4034,25 +3875,12 @@ function renderDirtyPlots() {
p.uplot.setScale('x', { min: plotNow - windowSec, max: plotNow }); p.uplot.setScale('x', { min: plotNow - windowSec, max: plotNow });
} }
zoomGuard = false; zoomGuard = false;
p._errCount = 0;
} catch (e) {
// One bad plot must not kill the whole render loop. Track consecutive
// failures and self-heal by rebuilding the uPlot instance.
p._errCount = (p._errCount || 0) + 1;
console.error(`[render] plot ${p.id}:`, e);
p.needsRedraw = true; // retry next frame
if (p._errCount >= 30) {
p._errCount = 0;
try { createUPlot(p); } catch (e2) { console.error(`[render] rebuild plot ${p.id}:`, e2); }
}
}
}); });
// Keep per-plot cursor value readouts in sync with live data. // Keep per-plot cursor value readouts in sync with live data.
if (cursors.mode === 'on') updatePlotCursorReadouts(); if (cursors.mode === 'on') updatePlotCursorReadouts();
} catch (e) {
console.error('[render]', e); requestAnimationFrame(renderDirtyPlots);
}
} }
@@ -4130,7 +3958,6 @@ function onSources(msg) {
}); });
buildSidebar(); buildSidebar();
if (statsOpen) _refreshStatsSelector(); if (statsOpen) _refreshStatsSelector();
maybeRestoreViewLate();
} }
function addSourceWS(label, addr, multicastGroup, dataPort) { function addSourceWS(label, addr, multicastGroup, dataPort) {
@@ -4755,303 +4582,7 @@ initSignalMenu();
const cb = document.getElementById('cb-monotonic'); const cb = document.getElementById('cb-monotonic');
if (cb) cb.checked = localStorage.getItem('udpscope.monotonic') === '1'; if (cb) cb.checked = localStorage.getItem('udpscope.monotonic') === '1';
} }
// Export every stored sample of the plotted signals as a Parquet file, served document.getElementById('btn-csv-all').addEventListener('click', exportAllCSV);
// by the Go hub's /api/export. Full resolution (no decimation) and hole-free
// (each signal keeps its own timestamps — long format). The file can be huge
// (hundreds of MB at high rates), so stream it to disk when the File System
// Access API is available instead of holding it in a Blob.
async function exportParquet() {
if (exportBusy) return;
const inTrigMode = trig.enabled && trig.snapshot !== null;
const keys = [];
plots.forEach(p => p.traces.forEach(k => { if (!keys.includes(k)) keys.push(k); }));
if (!keys.length) return;
// Same range resolution as the CSV export.
let t0, t1;
if (inTrigMode) {
t0 = trig.trigTime - activePreSec();
t1 = trig.trigTime + activePostSec();
} else {
const refPlot = plots.find(p => p.xRange);
if (refPlot) {
[t0, t1] = refPlot.xRange;
} else {
let plotNow = -Infinity;
keys.forEach(k => {
const buf = buffers[k];
if (buf && buf.size > 0) {
const t = buf.t[(buf.head - 1 + buf.cap) % buf.cap];
if (t > plotNow) plotNow = t;
}
});
if (!isFinite(plotNow)) plotNow = Date.now() / 1000;
t0 = plotNow - windowSec;
t1 = plotNow;
}
}
if (!(t1 > t0)) return;
exportBusy = true;
setExportBusy(true);
try {
const url = '/api/export?t0=' + t0.toFixed(9) + '&t1=' + t1.toFixed(9) +
'&signals=' + encodeURIComponent(keys.join(','));
const resp = await fetch(url);
if (!resp.ok) {
alert('Parquet export failed (HTTP ' + resp.status + ').\n\n' +
'The /api/export endpoint is provided by the Go hub; the C++ ' +
'StreamHub does not serve it.');
return;
}
const filename = 'signals_' + Date.now() + '.parquet';
if (window.showSaveFilePicker && resp.body) {
try {
const handle = await window.showSaveFilePicker({
suggestedName: filename,
types: [{ description: 'Parquet', accept: { 'application/vnd.apache.parquet': ['.parquet'] } }],
});
const writable = await handle.createWritable();
await resp.body.pipeTo(writable);
return;
} catch (e) {
if (e && e.name === 'AbortError') return; // user cancelled the picker
console.warn('parquet export: file picker failed, falling back to Blob', e);
}
}
const blob = await resp.blob();
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = filename;
a.click();
URL.revokeObjectURL(a.href);
} catch (e) {
console.warn('parquet export failed', e);
alert('Parquet export failed: ' + e.message);
} finally {
setExportBusy(false);
}
}
// Export dropdown: dispatch on selection, then reset to the placeholder so the
// same format can be chosen again.
document.getElementById('export-select').addEventListener('change', () => {
const sel = document.getElementById('export-select');
const fmt = sel.value;
sel.value = '';
if (fmt === 'csv') exportAllCSV();
else if (fmt === 'parquet') exportParquet();
});
/* ════════════════════════════════════════════════════════════════
View-state persistence (cookie)
════════════════════════════════════════════════════════════════ */
// The whole client view — layout, plots (traces/titles/modes), window, trigger
// configuration, rulers, sources — is serialised into one cookie so a reload
// restores the previous view. Cookies are size-limited, so the state degrades
// gracefully (rulers → trigger → sources → traces) when it would not fit.
const VIEW_COOKIE = 'udpscope.view';
const VIEW_COOKIE_MAX = 3500; // encoded chars; browsers cap cookies at ~4 KiB
function packViewState() {
const state = {
v: 1,
windowSec: windowSec,
layout: currentLayout,
plots: plots.map(p => ({
title: p.title,
mode: p.mode,
traces: p.traces.map(k => {
const colon = k.indexOf(':');
const name = colon >= 0 ? k.slice(colon + 1) : k;
return { key: k, label: srcLabelForKey(k), name };
}),
})),
trig: {
enabled: trig.enabled, signal: trig.signal, edge: trig.edge,
threshold: trig.threshold, windowSec: trig.windowSec,
prePercent: trig.prePercent, mode: trig.mode, holdoffSec: trig.holdoffSec,
},
rulers: {
mode: rulers.mode,
plotId: plots.findIndex(p => p.id === rulers.plotId),
states: plots.map(p => {
const rs = rulerState[p.id];
return rs ? { yA: rs.yA, yB: rs.yB } : { yA: null, yB: null };
}),
},
sources: Object.values(sourcesMap).map(s => ({
label: s.label || s.addr || s.id, addr: s.addr,
})),
};
let s = JSON.stringify(state);
const tooBig = () => encodeURIComponent(s).length > VIEW_COOKIE_MAX;
if (tooBig()) { delete state.rulers; s = JSON.stringify(state); }
if (tooBig()) { delete state.trig; s = JSON.stringify(state); }
if (tooBig()) { delete state.sources; s = JSON.stringify(state); }
if (tooBig()) {
state.plots = state.plots.map(p => ({ title: p.title, mode: p.mode }));
s = JSON.stringify(state);
}
if (tooBig()) { state.plots = []; s = JSON.stringify(state); }
return s;
}
// Saves are gated until the saved view has been re-applied (phase 2) or the
// grace timeout fires: otherwise the very first periodic save would overwrite
// the cookie with the not-yet-restored (empty) state and destroy it.
let _viewSaveReady = false;
function saveViewState() {
if (!_viewSaveReady) return;
try {
const s = packViewState();
document.cookie = VIEW_COOKIE + '=' + encodeURIComponent(s) +
'; path=/; max-age=31536000; SameSite=Lax';
} catch (e) {
console.warn('view cookie save failed', e);
}
}
function readViewState() {
try {
const prefix = VIEW_COOKIE + '=';
const m = document.cookie.split('; ').find(c => c.startsWith(prefix));
if (!m) return null;
const st = JSON.parse(decodeURIComponent(m.slice(prefix.length)));
return (st && st.v === 1) ? st : null;
} catch (e) {
return null;
}
}
// Phase 1 (init): layout, plot cards (titles/modes), window, rulers. Traces and
// the trigger need sources/signals loaded, so they are applied in phase 2.
function restoreViewState() {
const st = readViewState();
if (!st) return;
if (st.layout && LAYOUTS.some(l => l[1] === st.layout)) applyLayout(st.layout);
const plotState = st.plots || [];
plotState.forEach((ps, i) => {
const p = plots[i];
if (!p) return;
if (ps.title && ps.title !== 'Plot ' + p.id) {
p.title = ps.title;
const tEl = document.getElementById('ptitle-' + p.id);
if (tEl) tEl.textContent = ps.title;
const inp = document.querySelector('#pcfg-' + p.id + ' .pcfg-title-input');
if (inp) inp.value = ps.title;
}
if (ps.mode && ps.mode !== p.mode) {
p.mode = ps.mode;
document.querySelectorAll('#pcfg-' + p.id + ' .pcfg-mode-btn')
.forEach(b => b.classList.toggle('active', b.dataset.mode === ps.mode));
}
});
if (st.windowSec != null) {
windowSec = st.windowSec;
const sel = document.getElementById('window-select');
if (sel && [...sel.options].some(o => o.value === String(st.windowSec))) {
sel.value = String(st.windowSec);
}
}
if (st.rulers) {
rulers.mode = st.rulers.mode === 'on' ? 'on' : 'off';
const btn = document.getElementById('btn-ruler');
if (btn) btn.classList.toggle('active', rulers.mode === 'on');
(st.rulers.states || []).forEach((rs, i) => {
const p = plots[i];
if (!p || !rs) return;
const cur = getRulerState(p.id);
cur.yA = rs.yA; cur.yB = rs.yB;
});
if (st.rulers.plotId != null && plots[st.rulers.plotId]) {
rulers.plotId = plots[st.rulers.plotId].id;
}
}
}
// Rebuild a saved trace key against the current sources: source ids change
// across restarts, so match by label and fall back to the saved id-key.
function restoreTraceKey(entry) {
const src = Object.values(sourcesMap).find(s => (s.label || s.id) === entry.label);
return src ? (src.id + ':' + entry.name) : entry.key;
}
// Phase 2 (first sources + signals): reconcile sources, re-apply traces, then
// the trigger configuration and the window to the hub.
let _viewLateRestored = false;
function maybeRestoreViewLate() {
if (_viewLateRestored) return;
const st = readViewState();
if (!st) { _viewLateRestored = true; return; }
// Add any saved sources the hub does not already have (it persists its own).
const known = Object.values(sourcesMap).map(s => (s.label || s.addr || s.id) + '\u0000' + s.addr);
let added = false;
(st.sources || []).forEach(sv => {
const k = (sv.label || sv.addr) + '\u0000' + (sv.addr || '');
if (!known.includes(k)) { addSourceWS(sv.label, sv.addr, sv.multicastGroup, sv.dataPort); added = true; }
});
if (added) return; // re-enter when the new sources appear
// Traces and the trigger selector need at least one source with signals.
if (!Object.values(sourcesMap).some(s => (s.signals || []).length > 0)) return;
_viewLateRestored = true;
_viewSaveReady = true;
(st.plots || []).forEach((ps, i) => {
const p = plots[i];
if (!p) return;
(ps.traces || []).forEach(t => addTraceTo(p.id, restoreTraceKey(t)));
});
if (st.windowSec != null) {
windowSec = st.windowSec;
sendWindow();
}
const t = st.trig;
if (t) {
trig.edge = t.edge || trig.edge;
if (t.threshold != null) trig.threshold = t.threshold;
if (t.windowSec != null) trig.windowSec = t.windowSec;
if (t.prePercent != null) trig.prePercent = t.prePercent;
trig.mode = t.mode || trig.mode;
if (t.holdoffSec != null) trig.holdoffSec = t.holdoffSec;
trig.signal = t.signal || '';
const el = id => document.getElementById(id);
if (el('trig-edge')) el('trig-edge').value = trig.edge;
if (el('trig-window')) el('trig-window').value = String(trig.windowSec);
if (el('trig-mode')) el('trig-mode').value = trig.mode;
if (el('trig-holdoff')) el('trig-holdoff').value = trig.holdoffSec;
if (el('trig-pre')) el('trig-pre').value = String(trig.prePercent);
if (el('trig-pre-val')) el('trig-pre-val').textContent = trig.prePercent + '%';
refreshTrigThresholdField();
const selSig = document.getElementById('trig-signal');
if (selSig && trig.signal) {
const base = trig.signal.replace(/\[\d+\]$/, '');
if ([...selSig.options].some(o => o.value === base)) selSig.value = base;
}
if (t.enabled) openTrigBar(true);
else updateTrigStatusBadge('idle');
}
}
// Periodic save keeps the cookie current without wiring every control; the
// pagehide save captures the final state on close/reload.
setInterval(saveViewState, 3000);
window.addEventListener('pagehide', saveViewState);
// Hub down / never connected: stop gating after 10 s so layout, window and
// rulers still persist even though traces and the trigger could not be
// restored.
setTimeout(() => { _viewSaveReady = true; }, 10000);
document.getElementById('history-badge').addEventListener('click', toggleHistoryPanel); document.getElementById('history-badge').addEventListener('click', toggleHistoryPanel);
document.getElementById('btn-hist-cancel').addEventListener('click', toggleHistoryPanel); document.getElementById('btn-hist-cancel').addEventListener('click', toggleHistoryPanel);
document.getElementById('btn-hist-apply').addEventListener('click', applyHistoryBudget); document.getElementById('btn-hist-apply').addEventListener('click', applyHistoryBudget);
@@ -5061,7 +4592,6 @@ document.getElementById('stats-source-sel').addEventListener('change', e => {
statsSelectedSrc = e.target.value || null; statsSelectedSrc = e.target.value || null;
renderStats(); renderStats();
}); });
restoreViewState();
resolveHub().then(connectWS); resolveHub().then(connectWS);
requestAnimationFrame(renderDirtyPlots); requestAnimationFrame(renderDirtyPlots);
fetch('/version').then(r => r.text()).then(v => { fetch('/version').then(r => r.text()).then(v => {
+1 -5
View File
@@ -41,11 +41,7 @@
<button id="btn-ruler" class="ctrl-btn" title="Horizontal value rulers">Rulers</button> <button id="btn-ruler" class="ctrl-btn" title="Horizontal value rulers">Rulers</button>
<button id="btn-zoom-back" class="ctrl-btn" style="display:none">← Back</button> <button id="btn-zoom-back" class="ctrl-btn" style="display:none">← Back</button>
<button id="btn-zoom-fit" class="ctrl-btn">Fit</button> <button id="btn-zoom-fit" class="ctrl-btn">Fit</button>
<select id="export-select" class="ctrl-select" title="Export the visible signals"> <button id="btn-csv-all" class="ctrl-btn" title="Export all signals to CSV">⬇ CSV</button>
<option value="" disabled selected>⬇ Export</option>
<option value="csv" title="Export the visible signals as CSV (decimated to a bounded row count)">CSV</option>
<option value="parquet" title="Export every stored sample (full resolution, no holes) as Parquet — requires the Go hub">Parquet</option>
</select>
<button id="btn-sync-resume" class="ctrl-btn resume-btn" style="display:none">↺ Auto</button> <button id="btn-sync-resume" class="ctrl-btn resume-btn" style="display:none">↺ Auto</button>
<button id="btn-trigger" class="ctrl-btn">⚡ Trigger</button> <button id="btn-trigger" class="ctrl-btn">⚡ Trigger</button>
<button id="btn-pause-global" class="ctrl-btn">⏸ Pause</button> <button id="btn-pause-global" class="ctrl-btn">⏸ Pause</button>
-8
View File
@@ -14,11 +14,6 @@
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html, body { height:100%; background:var(--bg); color:var(--text); html, body { height:100%; background:var(--bg); color:var(--text);
font-family:'Segoe UI',system-ui,sans-serif; font-size:14px; overflow:hidden; } font-family:'Segoe UI',system-ui,sans-serif; font-size:14px; overflow:hidden; }
/* Uniform 0.9x compaction — scales every element (fonts, bars, plots,
spacing) while reflowing layout. `zoom` (Chrome/Edge/Safari, Firefox 126+)
is preferred over `transform: scale` because it reflows, so fixed-position
bars and JS-computed offsets stay aligned. */
html { zoom: 0.9; }
::-webkit-scrollbar { width:6px; } ::-webkit-scrollbar { width:6px; }
::-webkit-scrollbar-track { background:var(--mantle); } ::-webkit-scrollbar-track { background:var(--mantle); }
::-webkit-scrollbar-thumb { background:var(--surface1); border-radius:3px; } ::-webkit-scrollbar-thumb { background:var(--surface1); border-radius:3px; }
@@ -261,9 +256,6 @@ input[type=range].trig-range::-webkit-slider-thumb {
#plot-grid.l2x3 { grid-template-columns:1fr 1fr; grid-template-rows:1fr 1fr 1fr; } #plot-grid.l2x3 { grid-template-columns:1fr 1fr; grid-template-rows:1fr 1fr 1fr; }
#plot-grid.l1x4 { grid-template-columns:1fr; grid-template-rows:1fr 1fr 1fr 1fr; } #plot-grid.l1x4 { grid-template-columns:1fr; grid-template-rows:1fr 1fr 1fr 1fr; }
#plot-grid.l4x1 { grid-template-columns:1fr 1fr 1fr 1fr; grid-template-rows:1fr; } #plot-grid.l4x1 { grid-template-columns:1fr 1fr 1fr 1fr; grid-template-rows:1fr; }
/* 1+2 layout: one plot spanning the top row, two side by side below. */
#plot-grid.l1p2 { grid-template-columns:1fr 1fr; grid-template-rows:1fr 1fr; }
#plot-grid.l1p2 .plot-card:first-child { grid-column: 1 / -1; }
/* ── Plot card ────────────────────────────────────────────────── */ /* ── Plot card ────────────────────────────────────────────────── */
.plot-card { .plot-card {
+5 -26
View File
@@ -482,36 +482,11 @@ static int decode_data(udps_client_t *c, const uint8_t *pl, size_t len,
size_t total = 0u; size_t total = 0u;
size_t written = 0u; size_t written = 0u;
uint32_t i; uint32_t i;
uint32_t lost = 0u;
udps_frame_t frame; udps_frame_t frame;
if (c->num_sigs == 0u) { if (c->num_sigs == 0u) {
return 0; /* DATA before CONFIG: nothing to decode against. */ return 0; /* DATA before CONFIG: nothing to decode against. */
} }
/* Order the sequence before spending anything on the payload.
*
* Reassembly completes in arrival order, not counter order, so a packet
* delayed or duplicated on the wire surfaces after a newer one has already
* been delivered. Its samples carry an older time base: they land on top
* of data the consumer already has and leave the span they should have
* filled empty. Nothing in the payload distinguishes such a packet from a
* good one, only the counter does.
*
* The counter is a wrapping uint32, so it is ordered by the signed
* difference; comparing the values directly would call the first packet
* after the wrap stale and reject the stream from then on. */
if (c->have_counter) {
int32_t delta = (int32_t)(counter - c->last_counter);
if (delta <= 0) {
c->stats.stale_packets++;
return 0;
}
lost = (uint32_t)delta - 1u;
c->stats.counter_gaps += lost;
}
c->last_counter = counter;
c->have_counter = 1;
if (len < 8u) { if (len < 8u) {
return fail(c, "DATA payload too short (%lu bytes)", (unsigned long)len); return fail(c, "DATA payload too short (%lu bytes)", (unsigned long)len);
} }
@@ -553,11 +528,15 @@ static int decode_data(udps_client_t *c, const uint8_t *pl, size_t len,
written += count; written += count;
} }
if (c->have_counter && counter > c->last_counter + 1u) {
c->stats.counter_gaps += counter - c->last_counter - 1u;
}
c->last_counter = counter;
c->have_counter = 1;
c->stats.frames_delivered++; c->stats.frames_delivered++;
if (c->on_data != NULL) { if (c->on_data != NULL) {
frame.counter = counter; frame.counter = counter;
frame.lost = lost;
frame.hrt = rd_u64(pl); frame.hrt = rd_u64(pl);
frame.recv_time = recv_time; frame.recv_time = recv_time;
frame.publish_mode = c->publish_mode; frame.publish_mode = c->publish_mode;
+1 -18
View File
@@ -52,8 +52,7 @@ enum {
UDPS_PKT_CONFIG = 1, /**< Server -> client: signal metadata. */ UDPS_PKT_CONFIG = 1, /**< Server -> client: signal metadata. */
UDPS_PKT_ACK = 2, /**< Client -> server: keepalive. */ UDPS_PKT_ACK = 2, /**< Client -> server: keepalive. */
UDPS_PKT_CONNECT = 3, /**< Client -> server: open a session. */ UDPS_PKT_CONNECT = 3, /**< Client -> server: open a session. */
UDPS_PKT_DISCONNECT = 4, /**< Either direction: close a session. */ UDPS_PKT_DISCONNECT = 4 /**< Either direction: close a session. */
UDPS_PKT_SUBSCRIBE = 5 /**< Client to TCP stream service only: payload = stream name(s), comma separated; server frames carry the stream index in fragmentIdx. */
}; };
/** Sample type codes (udps_signal_t::type_code). */ /** Sample type codes (udps_signal_t::type_code). */
@@ -141,16 +140,6 @@ typedef struct {
/** One fully decoded DATA packet. */ /** One fully decoded DATA packet. */
typedef struct { typedef struct {
uint32_t counter; /**< Packet counter; gaps mean lost datagrams. */ uint32_t counter; /**< Packet counter; gaps mean lost datagrams. */
/**
* DATA packets missing immediately before this one, from the counter.
*
* Needed to space samples correctly: the elapsed time since the previous
* frame covers the lost packets' cycles too, so dividing it by this
* frame's sample count alone gives a period too long by exactly
* @c lost + 1, which walks the samples past their own end and into the
* range the next frame claims.
*/
uint32_t lost;
uint64_t hrt; /**< Producer's high-resolution timer at send. */ uint64_t hrt; /**< Producer's high-resolution timer at send. */
double recv_time; /**< Wall-clock seconds (CLOCK_REALTIME) at arrival. */ double recv_time; /**< Wall-clock seconds (CLOCK_REALTIME) at arrival. */
uint8_t publish_mode; /**< UDPS_PUBLISH_*. */ uint8_t publish_mode; /**< UDPS_PUBLISH_*. */
@@ -175,12 +164,6 @@ typedef struct {
uint64_t config_updates; uint64_t config_updates;
uint64_t fragments_dropped; /**< Duplicate, stale or unplaceable fragments. */ uint64_t fragments_dropped; /**< Duplicate, stale or unplaceable fragments. */
uint64_t counter_gaps; /**< DATA packets missing from the sequence. */ uint64_t counter_gaps; /**< DATA packets missing from the sequence. */
/**
* DATA packets dropped for not advancing the counter: reordered or
* duplicated on the wire. Delivering one would stamp its values with a
* time base older than data already handed over.
*/
uint64_t stale_packets;
uint64_t reconnects; uint64_t reconnects;
} udps_stats_t; } udps_stats_t;
+2 -14
View File
@@ -1,19 +1,7 @@
module marte2/common module marte2/common
go 1.24.9 go 1.21
require github.com/gorilla/websocket v1.5.1 require github.com/gorilla/websocket v1.5.1
require ( require golang.org/x/net v0.17.0 // indirect
github.com/andybalholm/brotli v1.1.1 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/klauspost/compress v1.17.9 // indirect
github.com/parquet-go/bitpack v1.0.0 // indirect
github.com/parquet-go/jsonlite v1.0.0 // indirect
github.com/parquet-go/parquet-go v0.32.0 // indirect
github.com/pierrec/lz4/v4 v4.1.21 // indirect
github.com/twpayne/go-geom v1.6.1 // indirect
golang.org/x/net v0.17.0 // indirect
golang.org/x/sys v0.38.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect
)
-21
View File
@@ -1,25 +1,4 @@
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/parquet-go/bitpack v1.0.0 h1:AUqzlKzPPXf2bCdjfj4sTeacrUwsT7NlcYDMUQxPcQA=
github.com/parquet-go/bitpack v1.0.0/go.mod h1:XnVk9TH+O40eOOmvpAVZ7K2ocQFrQwysLMnc6M/8lgs=
github.com/parquet-go/jsonlite v1.0.0 h1:87QNdi56wOfsE5bdgas0vRzHPxfJgzrXGml1zZdd7VU=
github.com/parquet-go/jsonlite v1.0.0/go.mod h1:nDjpkpL4EOtqs6NQugUsi0Rleq9sW/OtC1NnZEnxzF0=
github.com/parquet-go/parquet-go v0.32.0 h1:NWDqTUHfrCS4cJP/Fj2HlxvqsrVedWG3sayMkf+znzM=
github.com/parquet-go/parquet-go v0.32.0/go.mod h1:navtkAYr2LGoJVp141oXPlO/sxLvaOe3la2JEoD8+rg=
github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=
github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/twpayne/go-geom v1.6.1 h1:iLE+Opv0Ihm/ABIcvQFGIiFBXd76oBIar9drAwHFhR4=
github.com/twpayne/go-geom v1.6.1/go.mod h1:Kr+Nly6BswFsKM5sd31YaoWS5PeDDH2NftJTK7Gd028=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
@@ -1,146 +0,0 @@
package udpsprotocol
// Accumulate mode ships one full snapshot of EVERY signal per RT cycle —
// arrays included. See UDPStreamer.cpp pass 5 ("ALL signals (scalars and
// arrays alike) are tagged accumulated = true") and SerializeAccumulated,
// which writes, for each signal in CONFIG order, numSamples consecutive
// snapshots of that signal's full element set.
//
// The tests below build a payload byte-for-byte the way the C++ producer
// does, so a decoding regression shows up here rather than as a mangled
// waveform three components downstream.
import (
"encoding/binary"
"math"
"testing"
"time"
)
// buildAccumulatePayload lays out an Accumulate DATA payload exactly as
// UDPStreamer::SerializeAccumulated does:
//
// [8 HRT][4 numSamples] then, per signal, numSamples × NumElements float64.
//
// slots[i][k] holds signal i's element set for cycle k.
func buildAccumulatePayload(hrt uint64, slots [][][]float64) []byte {
numSamples := 0
if len(slots) > 0 {
numSamples = len(slots[0])
}
out := make([]byte, 12)
binary.LittleEndian.PutUint64(out[0:8], hrt)
binary.LittleEndian.PutUint32(out[8:12], uint32(numSamples))
for _, sig := range slots {
for _, elems := range sig {
for _, v := range elems {
var b [8]byte
binary.LittleEndian.PutUint64(b[:], math.Float64bits(v))
out = append(out, b[:]...)
}
}
}
return out
}
// TestParseDataAccumulateGivesEachSlotItsOwnArray pins the array case: with an
// accumulated batch, slot k's array signal must decode to the values the
// producer captured on cycle k, not to some other cycle's. Handing every slot
// slot 0's array would stamp one cycle's data with every slot's timestamp —
// the same samples drawn repeatedly at advancing times, with the cycles they
// displaced missing entirely.
func TestParseDataAccumulateGivesEachSlotItsOwnArray(t *testing.T) {
sigs := []SignalInfo{
{Name: "Time", TypeCode: 9, NumRows: 1, NumCols: 1, QuantType: QuantNone},
{Name: "Wave", TypeCode: 9, NumRows: 4, NumCols: 1, QuantType: QuantNone},
}
// Three RT cycles. "Wave" carries a different ramp each cycle so a
// mix-up is unambiguous.
timeSlots := [][]float64{{10}, {20}, {30}}
waveSlots := [][]float64{
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12},
}
payload := buildAccumulatePayload(777, [][][]float64{timeSlots, waveSlots})
samples, err := ParseData(payload, sigs, PublishModeAccumulate, time.Now())
if err != nil {
t.Fatalf("ParseData: %v", err)
}
if len(samples) != 3 {
t.Fatalf("expected 3 slots, got %d", len(samples))
}
for k, s := range samples {
got := s.Values["Wave"]
want := waveSlots[k]
if len(got) != len(want) {
t.Fatalf("slot %d: Wave has %d elements, want %d", k, len(got), len(want))
}
for i := range want {
if got[i] != want[i] {
t.Fatalf("slot %d: Wave = %v, want %v (slot %d's data has been "+
"served for this slot's timestamp)", k, got, want,
indexOfSlot(waveSlots, got))
}
}
if tv := s.Values["Time"]; len(tv) != 1 || tv[0] != timeSlots[k][0] {
t.Fatalf("slot %d: Time = %v, want %v", k, tv, timeSlots[k])
}
}
}
// TestParseDataAccumulateConsumesTheWholeArrayBlock catches the same defect
// from the other side: a signal following an array must be read at the right
// offset. Under-reading the array block slides every later signal backwards
// into the array's tail, which decodes as plausible-looking but wrong values
// rather than as an error.
func TestParseDataAccumulateConsumesTheWholeArrayBlock(t *testing.T) {
sigs := []SignalInfo{
{Name: "Wave", TypeCode: 9, NumRows: 4, NumCols: 1, QuantType: QuantNone},
{Name: "Tail", TypeCode: 9, NumRows: 1, NumCols: 1, QuantType: QuantNone},
}
waveSlots := [][]float64{
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12},
}
tailSlots := [][]float64{{100}, {200}, {300}}
payload := buildAccumulatePayload(0, [][][]float64{waveSlots, tailSlots})
samples, err := ParseData(payload, sigs, PublishModeAccumulate, time.Now())
if err != nil {
t.Fatalf("ParseData: %v", err)
}
if len(samples) != 3 {
t.Fatalf("expected 3 slots, got %d", len(samples))
}
for k, s := range samples {
tv := s.Values["Tail"]
if len(tv) != 1 || tv[0] != tailSlots[k][0] {
t.Fatalf("slot %d: Tail = %v, want %v — the array block before it "+
"was not fully consumed", k, tv, tailSlots[k])
}
}
}
// indexOfSlot reports which slot's data a decoded array actually matches, so a
// failure message can name the culprit instead of just showing numbers.
func indexOfSlot(slots [][]float64, got []float64) int {
for k, want := range slots {
if len(want) != len(got) {
continue
}
same := true
for i := range want {
if want[i] != got[i] {
same = false
break
}
}
if same {
return k
}
}
return -1
}
+33 -39
View File
@@ -18,7 +18,6 @@ const (
PktACK uint8 = 2 PktACK uint8 = 2
PktConnect uint8 = 3 PktConnect uint8 = 3
PktDisconnect uint8 = 4 PktDisconnect uint8 = 4
PktSubscribe uint8 = 5 // TCP stream service only: payload = stream name(s), comma separated; server frames carry the stream index in fragmentIdx
HeaderSize = 17 HeaderSize = 17
SigDescSize = 136 SigDescSize = 136
@@ -291,37 +290,28 @@ type DataSample struct {
HRTTimestamp uint64 HRTTimestamp uint64
WallTime time.Time // wall-clock time at UDP arrival; used as x-axis WallTime time.Time // wall-clock time at UDP arrival; used as x-axis
Values map[string][]float64 // key = signal name, value = []float64 with NumElements entries Values map[string][]float64 // key = signal name, value = []float64 with NumElements entries
// Lost is the number of DATA packets missing between the previous sample
// and this one, taken from the producer's packet counter (see
// SequenceGate). Consumers that derive a per-element period from the
// inter-packet gap need it: the gap widens with every lost packet, and
// dividing it by this packet's element count alone reports a period too
// long by exactly that factor — which walks the packet's elements past
// their own end and into the range the next packet claims.
Lost uint32
} }
// parseElems reads n elements for sig from payload at offset, advancing offset. // parseElems reads n elements for sig from payload at offset, advancing offset.
// Returns the slice of float64 values and the new offset. // Returns the slice of float64 values and the new offset.
func parseElems(payload []byte, offset, n int, sig SignalInfo) ([]float64, int, error) { func parseElems(payload []byte, offset, n int, sig SignalInfo) ([]float64, int, error) {
sz := rawTypeSize(sig.TypeCode)
if sig.QuantType != QuantNone {
sz = quantSize(sig.QuantType)
}
// Bounds-check before allocating. In Accumulate mode n is numSamples ×
// NumElements, so a malformed packet could otherwise ask for an allocation
// far larger than its own payload could ever justify.
if n < 0 || n > (len(payload)-offset)/sz {
return nil, offset, fmt.Errorf("data payload truncated for signal %q", sig.Name)
}
elems := make([]float64, n) elems := make([]float64, n)
needed := n * sz
if sig.QuantType == QuantNone { if sig.QuantType == QuantNone {
sz := rawTypeSize(sig.TypeCode)
needed := n * sz
if offset+needed > len(payload) {
return nil, offset, fmt.Errorf("data payload truncated for signal %q", sig.Name)
}
for i := 0; i < n; i++ { for i := 0; i < n; i++ {
elems[i] = readRawElement(payload, offset+i*sz, sig.TypeCode) elems[i] = readRawElement(payload, offset+i*sz, sig.TypeCode)
} }
offset += needed offset += needed
} else { } else {
sz := quantSize(sig.QuantType)
needed := n * sz
if offset+needed > len(payload) {
return nil, offset, fmt.Errorf("data payload truncated (quant) for signal %q", sig.Name)
}
for i := 0; i < n; i++ { for i := 0; i < n; i++ {
var raw uint16 var raw uint16
if sz == 1 { if sz == 1 {
@@ -341,13 +331,7 @@ func parseElems(payload []byte, offset, n int, sig SignalInfo) ([]float64, int,
// //
// For PublishModeAccumulate the payload format is: // For PublishModeAccumulate the payload format is:
// //
// [8 HRT][4 numSamples][for each signal: numSamples × NumElements elems] // [8 HRT][4 numSamples][for each signal: accumulated scalars → numSamples elems; arrays → NumElements elems]
//
// Every signal is accumulated, arrays included: the producer captures one full
// snapshot of the whole signal set per RT cycle and lays the cycles out
// contiguously per signal (UDPStreamer::SerializeAccumulated). Reading only
// NumElements for an array would hand every slot the first cycle's data and
// slide all later signals into that array's tail.
// //
// The function returns one DataSample per accumulated snapshot so the hub can // The function returns one DataSample per accumulated snapshot so the hub can
// process each slot independently with its own timestamp. // process each slot independently with its own timestamp.
@@ -373,18 +357,28 @@ func ParseData(payload []byte, sigs []SignalInfo, publishMode uint8, arrivalTime
} }
// Parse per-signal data blocks (all slots for a signal are contiguous). // Parse per-signal data blocks (all slots for a signal are contiguous).
accumVals := make(map[string][]float64, len(sigs)) // numSamples × NumElements accumVals := make(map[string][]float64, len(sigs)) // scalars: numSamples values
accumElems := make(map[string]int, len(sigs)) fixedVals := make(map[string][]float64, len(sigs)) // arrays: NumElements values
for _, sig := range sigs { for _, sig := range sigs {
n := sig.NumElements() n := sig.NumElements()
elems, newOff, err := parseElems(payload, offset, numSamples*n, sig) if n == 1 {
if err != nil { // Accumulated scalar: read numSamples back-to-back elements.
return nil, err elems, newOff, err := parseElems(payload, offset, numSamples, sig)
if err != nil {
return nil, err
}
offset = newOff
accumVals[sig.Name] = elems
} else {
// Fixed array (non-accumulated): one set of NumElements values.
elems, newOff, err := parseElems(payload, offset, n, sig)
if err != nil {
return nil, err
}
offset = newOff
fixedVals[sig.Name] = elems
} }
offset = newOff
accumVals[sig.Name] = elems
accumElems[sig.Name] = n
} }
// Build one DataSample per slot. // Build one DataSample per slot.
@@ -392,10 +386,10 @@ func ParseData(payload []byte, sigs []SignalInfo, publishMode uint8, arrivalTime
for k := 0; k < numSamples; k++ { for k := 0; k < numSamples; k++ {
vals := make(map[string][]float64, len(sigs)) vals := make(map[string][]float64, len(sigs))
for sigName, av := range accumVals { for sigName, av := range accumVals {
n := accumElems[sigName] vals[sigName] = []float64{av[k]}
// Sub-slice of the decoded block; the hub treats values as }
// read-only, so no copy is needed. for sigName, fv := range fixedVals {
vals[sigName] = av[k*n : (k+1)*n : (k+1)*n] vals[sigName] = fv // shared read-only reference; hub does not modify
} }
samples[k] = DataSample{HRTTimestamp: hrt, WallTime: arrivalTime, Values: vals} samples[k] = DataSample{HRTTimestamp: hrt, WallTime: arrivalTime, Values: vals}
} }
-49
View File
@@ -1,49 +0,0 @@
package udpsprotocol
// SequenceGate orders DATA packets by the producer's packet counter.
//
// Reassembly completes in arrival order, not counter order, so a packet that
// was delayed or duplicated on the wire is handed up after a newer one has
// already been consumed. Its samples then carry an older time base than the
// data already in the ring: they land on top of samples that are already
// there, and the span they should have filled stays empty. That is a hole on
// one side and a collision on the other, from a packet that is entirely
// well-formed — the counter is the only thing that distinguishes it.
//
// A SequenceGate is not safe for concurrent use; each receive loop owns one.
type SequenceGate struct {
last uint32
valid bool
// Stale counts packets rejected for not advancing the counter (reordered
// or duplicated), for diagnostics.
Stale uint64
}
// Reset forgets the sequence. Call it on (re)connect: the producer's counter
// restarts independently of ours, so a counter carried over from the previous
// connection would reject the whole new stream.
func (g *SequenceGate) Reset() {
g.last = 0
g.valid = false
}
// Accept reports whether a DATA packet with this counter should be delivered,
// and how many packets went missing immediately before it.
//
// The counter is a wrapping uint32, so ordering is done on the signed
// difference: a plain comparison would call the first packet after the wrap
// stale and reject everything from then on.
func (g *SequenceGate) Accept(counter uint32) (ok bool, lost uint32) {
if !g.valid {
g.valid = true
g.last = counter
return true, 0
}
delta := int32(counter - g.last)
if delta <= 0 {
g.Stale++
return false, 0
}
g.last = counter
return true, uint32(delta) - 1
}
@@ -1,82 +0,0 @@
package udpsprotocol
import "testing"
// A packet older than one already delivered carries an older time base. Its
// samples land on top of data that is already in the ring and leave the span
// they should have filled empty, so it must not get through.
func TestSequenceGateRejectsStaleAndDuplicate(t *testing.T) {
var g SequenceGate
if ok, lost := g.Accept(10); !ok || lost != 0 {
t.Fatalf("first packet: got (%v, %d), want (true, 0)", ok, lost)
}
if ok, _ := g.Accept(11); !ok {
t.Fatal("counter 11 advances past 10 and must be accepted")
}
if ok, _ := g.Accept(9); ok {
t.Error("counter 9 is older than the delivered 11 and must be dropped")
}
if ok, _ := g.Accept(11); ok {
t.Error("a repeat of the delivered counter must be dropped")
}
if g.Stale != 2 {
t.Errorf("Stale = %d, want 2", g.Stale)
}
// The rejections must not have moved the sequence on.
if ok, lost := g.Accept(12); !ok || lost != 0 {
t.Errorf("after rejections: got (%v, %d), want (true, 0)", ok, lost)
}
}
// The loss count is what lets a consumer tell a widened gap from a slowed
// producer, so it must exclude the packet being delivered and must not persist
// into the next one.
func TestSequenceGateReportsLoss(t *testing.T) {
var g SequenceGate
g.Accept(100)
if _, lost := g.Accept(104); lost != 3 {
t.Errorf("101..103 missing: lost = %d, want 3", lost)
}
if _, lost := g.Accept(105); lost != 0 {
t.Errorf("consecutive packet: lost = %d, want 0", lost)
}
if g.Stale != 0 {
t.Errorf("Stale = %d, want 0", g.Stale)
}
}
// The counter is a wrapping uint32. Ordering it by plain comparison would call
// every packet after the wrap older than 0xFFFFFFFF and kill the stream.
func TestSequenceGateSurvivesWraparound(t *testing.T) {
var g SequenceGate
for _, c := range []uint32{0xFFFFFFFD, 0xFFFFFFFE, 0xFFFFFFFF, 0, 1, 2} {
ok, lost := g.Accept(c)
if !ok {
t.Fatalf("counter %#x rejected across the wrap", c)
}
if lost != 0 {
t.Errorf("counter %#x: lost = %d, want 0", c, lost)
}
}
// Loss must still be measured correctly across the wrap.
var h SequenceGate
h.Accept(0xFFFFFFFE)
if _, lost := h.Accept(1); lost != 2 {
t.Errorf("0xFFFFFFFF and 0 missing: lost = %d, want 2", lost)
}
}
// A reconnect restarts the producer's counter independently of ours; a carried
// over counter would reject the entire new stream.
func TestSequenceGateResetAcceptsLowerCounter(t *testing.T) {
var g SequenceGate
g.Accept(5000)
g.Reset()
if ok, lost := g.Accept(3); !ok || lost != 0 {
t.Errorf("after Reset: got (%v, %d), want (true, 0)", ok, lost)
}
}
@@ -1,75 +0,0 @@
package wshub
import (
"math"
"testing"
)
// A short window at a high sample rate fits in a ring's initial capacity, so the
// retune sweep used to leave it there — and a ring holding exactly the window has
// already rolled past the front of a capture by the time that capture is read,
// which happens a post-window plus captureMarginSec after the trigger fires.
//
// 1 MSps over a 200 ms window: 200 k points fit in the 250 k initial ring, and
// every shot came back missing its first 123 ms.
func TestCaptureWholeAtHighRateShortWindow(t *testing.T) {
const (
key = "s1:Ch1"
rate = 1e6
window = 0.2
prePct = 20.0
batchSec = 1.0 / 30.0
simSec = 6.0
)
h := NewHub()
h.SetRingBudget(defaultRingPts)
h.rings[key] = newSigRing(ringCapInitial)
h.trigger.SetConfig(trigConfig{signalKey: key, edge: "rising", threshold: 0,
windowSec: window, prePercent: prePct, mode: "normal", holdoffSec: 0.2})
rateHz := float64(rate)
nBatch := int(rateHz * batchSec)
ts := make([]float64, nBatch)
vs := make([]float64, nBatch)
armed, shots := false, 0
for now := 0.0; now < simSec; now += batchSec {
for i := range ts {
ts[i] = now + float64(i)/rateHz
vs[i] = math.Sin(2 * math.Pi * 5 * ts[i]) // a rising crossing every 200 ms
}
h.ingest(key, 1, ts, vs)
h.retuneRings(now)
h.refreshTriggerFill()
if !armed && now > 2 {
h.trigger.Arm()
armed = true
}
trigTime, pre, post, ok := h.trigger.dueCapture(now + batchSec)
if !ok {
if h.trigger.dueRearm(now + batchSec) {
h.trigger.Arm()
}
continue
}
t0 := trigTime - pre
buf := h.buildTriggerCapture(trigTime, pre, post)
if buf == nil {
t.Fatalf("shot at t=%.4f produced no frame at all", trigTime)
}
first, last, n := decodeCaptureSpan(t, buf, key)
shots++
if lost := first - t0; lost > shortCaptureTol*window {
_, span := h.rings[key].stats()
t.Errorf("shot at t=%.4f is missing %.0f ms at the front of its %.0f ms window "+
"(got [%.4f,%.4f], %d pts; ring holds %.4f s in %d points)",
trigTime, 1e3*lost, 1e3*window, first, last, n, span, h.rings[key].capacity())
}
h.trigger.markTriggered(now + batchSec)
}
if shots < 3 {
t.Fatalf("only %d shots in %.0f s", shots, simSec)
}
}
-119
View File
@@ -1,119 +0,0 @@
package wshub
import (
"fmt"
"net/http"
"sort"
"strconv"
"strings"
"time"
"github.com/parquet-go/parquet-go"
)
// ExportSample is one row of the binary export: a single stored sample, in
// long ("tidy") form, keyed by source and signal with its own timestamp.
//
// Keeping each signal's samples as its own rows — rather than resampling onto a
// shared time grid — is what makes the export hole-free: per-signal streams of
// different lengths export exactly as stored, nothing is fabricated, and
// nothing is dropped.
type ExportSample struct {
Source string `parquet:"source"`
Signal string `parquet:"signal"`
Time float64 `parquet:"time"`
Value float64 `parquet:"value"`
}
// exportChunkRows bounds each batched write and, via MaxRowsPerRowGroup, the
// size of each parquet row group: memory stays bounded however large the
// export is, because a finished row group is flushed to the HTTP stream.
const exportChunkRows = 65536
// exportWriteBuffer is the parquet writer's output buffer: larger than the
// 32KiB default means fewer writes on the HTTP stream for a multi-GB export.
const exportWriteBuffer = 1 << 20
// HandleExport serves GET /api/export?t0=..&t1=..[&signals=a,b] as a Parquet
// file containing every stored sample of the named signals in [t0, t1].
//
// Unlike /api/zoom there is no decimation: the file holds the full contents of
// the rings. At rates above the ring budget those contents are min/max buckets
// (the finest resolution the hub retains); at lower rates they are verbatim.
func (h *Hub) HandleExport(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query()
t0, err0 := strconv.ParseFloat(q.Get("t0"), 64)
t1, err1 := strconv.ParseFloat(q.Get("t1"), 64)
if err0 != nil || err1 != nil || t1 <= t0 {
http.Error(w, "invalid t0/t1", http.StatusBadRequest)
return
}
var keys []string
if s := strings.TrimSpace(q.Get("signals")); s != "" {
keys = strings.Split(s, ",")
for i := range keys {
keys[i] = strings.TrimSpace(keys[i])
}
}
// Snapshot the rings we will read. A signal removed mid-export must not
// silently drop rows from the file.
h.ringsMu.RLock()
refs := make(map[string]*sigRing)
if keys == nil {
for k, rb := range h.rings {
refs[k] = rb
}
} else {
for _, k := range keys {
if rb, ok := h.rings[k]; ok {
refs[k] = rb
}
}
}
h.ringsMu.RUnlock()
if len(refs) == 0 {
http.Error(w, "no signals", http.StatusNotFound)
return
}
// Deterministic column order.
names := make([]string, 0, len(refs))
for k := range refs {
names = append(names, k)
}
sort.Strings(names)
w.Header().Set("Content-Type", "application/vnd.apache.parquet")
w.Header().Set("Content-Disposition",
fmt.Sprintf("attachment; filename=\"signals_%d.parquet\"", time.Now().Unix()))
writer := parquet.NewGenericWriter[ExportSample](w,
parquet.MaxRowsPerRowGroup(exportChunkRows),
parquet.WriteBufferSize(exportWriteBuffer),
)
batch := make([]ExportSample, 0, exportChunkRows)
for _, key := range names {
st, sv := refs[key].slice(t0, t1)
colon := strings.IndexByte(key, ':')
source, signal := key, key
if colon >= 0 {
source = key[:colon]
signal = key[colon+1:]
}
for i := range st {
batch = append(batch, ExportSample{Source: source, Signal: signal, Time: st[i], Value: sv[i]})
if len(batch) >= exportChunkRows {
if _, err := writer.Write(batch); err != nil {
// Client went away or the stream broke; stop writing.
return
}
batch = batch[:0]
}
}
}
if len(batch) > 0 {
_, _ = writer.Write(batch)
}
_ = writer.Close()
}
-87
View File
@@ -1,87 +0,0 @@
package wshub
import (
"bytes"
"net/http/httptest"
"testing"
"github.com/parquet-go/parquet-go"
)
func TestHandleExportParquetFullResolution(t *testing.T) {
h := NewHub()
// Two signals with different lengths and offset time bases: the export must
// keep every sample of each, on its own timestamps (no holes, no
// resampling, no decimation).
sig1 := newSigRing(10000)
sig2 := newSigRing(10000)
t1, v1 := make([]float64, 1000), make([]float64, 1000)
for i := range t1 {
t1[i] = float64(i) * 0.001
v1[i] = float64(i) * 2
}
sig1.write(t1, v1)
t2, v2 := make([]float64, 500), make([]float64, 500)
for i := range t2 {
t2[i] = 0.1 + float64(i)*0.002
v2[i] = -float64(i)
}
sig2.write(t2, v2)
h.rings["s1:Ch1"] = sig1
h.rings["s1:Ch2"] = sig2
req := httptest.NewRequest("GET", "/api/export?t0=0&t1=2&signals=s1:Ch1,s1:Ch2", nil)
rec := httptest.NewRecorder()
h.HandleExport(rec, req)
if rec.Code != 200 {
t.Fatalf("status = %d, want 200 (body: %s)", rec.Code, rec.Body.String())
}
reader := parquet.NewGenericReader[ExportSample](bytes.NewReader(rec.Body.Bytes()))
defer reader.Close()
var got []ExportSample
buf := make([]ExportSample, 1000)
for {
n, err := reader.Read(buf)
got = append(got, buf[:n]...)
if err != nil {
break
}
}
if len(got) != 1500 {
t.Fatalf("rows = %d, want 1500 (every sample of both signals)", len(got))
}
ch1 := filterExportSamples(got, "s1", "Ch1")
ch2 := filterExportSamples(got, "s1", "Ch2")
if len(ch1) != 1000 || len(ch2) != 500 {
t.Fatalf("ch1=%d ch2=%d rows, want 1000/500 (no holes, no resampling)", len(ch1), len(ch2))
}
if ch1[0].Time != 0 || ch1[0].Value != 0 || ch1[999].Time != 0.999 || ch1[999].Value != 1998 {
t.Fatalf("ch1 endpoints wrong: first=%+v last=%+v", ch1[0], ch1[999])
}
if ch2[0].Time != 0.1 || ch2[499].Time != 0.1+499*0.002 || ch2[499].Value != -499 {
t.Fatalf("ch2 endpoints wrong: first=%+v last=%+v", ch2[0], ch2[499])
}
}
func filterExportSamples(rows []ExportSample, source, signal string) []ExportSample {
out := make([]ExportSample, 0, len(rows))
for _, r := range rows {
if r.Source == source && r.Signal == signal {
out = append(out, r)
}
}
return out
}
func TestHandleExportParquetBadRange(t *testing.T) {
h := NewHub()
h.rings["s1:Ch1"] = newSigRing(10)
req := httptest.NewRequest("GET", "/api/export?t0=2&t1=1", nil)
rec := httptest.NewRecorder()
h.HandleExport(rec, req)
if rec.Code != 400 {
t.Fatalf("status = %d, want 400 for inverted range", rec.Code)
}
}
+17 -26
View File
@@ -422,26 +422,6 @@ func (hw *historyWriter) window() float64 {
return hw.windowSec return hw.windowSec
} }
// coversWindow reports whether the archive file for key currently spans at
// least sec seconds. When true, backfillCaptureHead can reconstruct a capture's
// front out of the archive, so the trigger need not wait for the ring to cover
// the whole window on its own.
func (hw *historyWriter) coversWindow(key string, sec float64) bool {
if !hw.enabled() || !(sec > 0) {
return false
}
hw.mu.RLock()
hf, ok := hw.files[key]
hw.mu.RUnlock()
if !ok {
return false
}
hf.mu.RLock()
span := hf.tNewest - hf.tOldest
hf.mu.RUnlock()
return span >= sec
}
// setWindow points the archive at the timespan the clients are looking at, and // setWindow points the archive at the timespan the clients are looking at, and
// re-sizes the files that no longer match it. It reports whether any file's // re-sizes the files that no longer match it. It reports whether any file's
// geometry changed, which invalidates what clients know about the archive. // geometry changed, which invalidates what clients know about the archive.
@@ -862,7 +842,10 @@ func (hf *histFile) readAfter(after, t0, t1 float64, max int) ([]byte, float64,
// The run wraps at most once, so it costs at most two reads. // The run wraps at most once, so it costs at most two reads.
buf := make([]byte, n*histPairSize) buf := make([]byte, n*histPairSize)
start := (oldest + lo) % capacity start := (oldest + lo) % capacity
head := min(int(capacity-start)*histPairSize, len(buf)) head := int(capacity-start) * histPairSize
if head > len(buf) {
head = len(buf)
}
if _, err := hf.f.ReadAt(buf[:head], int64(histHeaderSize)+int64(start)*histPairSize); err != nil { if _, err := hf.f.ReadAt(buf[:head], int64(histHeaderSize)+int64(start)*histPairSize); err != nil {
return nil, 0, err return nil, 0, err
} }
@@ -915,8 +898,10 @@ func (hf *histFile) writePairs(t, v []float64) error {
binary.LittleEndian.PutUint64(buf[i*histPairSize+8:], math.Float64bits(v[i])) binary.LittleEndian.PutUint64(buf[i*histPairSize+8:], math.Float64bits(v[i]))
} }
first := min(int(hf.capacity-hf.head), n) first := int(hf.capacity - hf.head)
if first > n {
first = n
}
off := int64(histHeaderSize) + int64(hf.head)*histPairSize off := int64(histHeaderSize) + int64(hf.head)*histPairSize
if _, err := hf.f.WriteAt(buf[:first*histPairSize], off); err != nil { if _, err := hf.f.WriteAt(buf[:first*histPairSize], off); err != nil {
return err return err
@@ -1104,7 +1089,10 @@ func (hw *historyWriter) readRange(key string, t0, t1 float64, maxOut int) ([]fl
// Read in contiguous runs: the range wraps at most once. // Read in contiguous runs: the range wraps at most once.
buf := make([]byte, n*histPairSize) buf := make([]byte, n*histPairSize)
start := (oldest + lo) % capacity start := (oldest + lo) % capacity
first := min(int(capacity-start), n) first := int(capacity - start)
if first > n {
first = n
}
if _, err := hf.f.ReadAt(buf[:first*histPairSize], if _, err := hf.f.ReadAt(buf[:first*histPairSize],
int64(histHeaderSize)+int64(start)*histPairSize); err != nil { int64(histHeaderSize)+int64(start)*histPairSize); err != nil {
return nil, nil return nil, nil
@@ -1277,7 +1265,7 @@ func (h *Hub) handleSetHistoryBudget(env map[string]interface{}) {
// handleHistoryZoom answers a historyZoom request from disk. Same request and // handleHistoryZoom answers a historyZoom request from disk. Same request and
// reply shape as "zoom", so clients can fall back to it transparently when a // reply shape as "zoom", so clients can fall back to it transparently when a
// window reaches further back than the in-memory rings hold. // window reaches further back than the in-memory rings hold.
func (h *Hub) handleHistoryZoom(c *wsClient, env map[string]any) { func (h *Hub) handleHistoryZoom(c *wsClient, env map[string]interface{}) {
if !h.hist.enabled() { if !h.hist.enabled() {
msg, _ := json.Marshal(map[string]any{ msg, _ := json.Marshal(map[string]any{
"type": "historyZoom", "reqId": env["reqId"], "type": "historyZoom", "reqId": env["reqId"],
@@ -1299,7 +1287,10 @@ func (h *Hub) handleHistoryZoom(c *wsClient, env map[string]any) {
// oversampled relative to the plot's point budget and thinned afterwards. // oversampled relative to the plot's point budget and thinned afterwards.
// The cap keeps a request for "no decimation" over a multi-hour window from // The cap keeps a request for "no decimation" over a multi-hour window from
// pulling the whole file into memory. // pulling the whole file into memory.
readCap := min(n*histReadOversample, histDefaultMaxPoints) readCap := n * histReadOversample
if readCap > histMaxReadPoints {
readCap = histMaxReadPoints
}
signals := make(map[string]sigData) signals := make(map[string]sigData)
for _, k := range strings.Split(sigCSV, ",") { for _, k := range strings.Split(sigCSV, ",") {
+3 -12
View File
@@ -1216,24 +1216,15 @@ func (h *Hub) buildBinaryDataMessageForSource(src *sourceHubState, batch []udpsp
wallNs := s.WallTime.UnixNano() wallNs := s.WallTime.UnixNano()
wallSec := float64(wallNs) / 1e9 wallSec := float64(wallNs) / 1e9
var dtSec float64 var dtSec float64
// A gap spans the elements of every packet that went missing
// inside it as well as this packet's own, so the divisor has
// to widen with it. Without this a single loss halves the
// apparent rate and the elements overrun into the next
// packet's range. The loss count belongs to the packet the
// gap ends at.
if bi+1 < len(batch) { if bi+1 < len(batch) {
// Two consecutive packets in this tick → exact dt. // Two consecutive packets in this tick → exact dt.
span := float64(n) * float64(1+batch[bi+1].Lost) dtSec = (float64(batch[bi+1].WallTime.UnixNano()) - float64(wallNs)) / 1e9 / float64(n)
dtSec = (float64(batch[bi+1].WallTime.UnixNano()) - float64(wallNs)) / 1e9 / span
} else if bi > 0 { } else if bi > 0 {
// Last of multiple packets → use diff from previous. // Last of multiple packets → use diff from previous.
span := float64(n) * float64(1+s.Lost) dtSec = (float64(wallNs) - float64(batch[bi-1].WallTime.UnixNano())) / 1e9 / float64(n)
dtSec = (float64(wallNs) - float64(batch[bi-1].WallTime.UnixNano())) / 1e9 / span
} else if prevNs, ok2 := src.lastPktNs[sig.Name]; ok2 && prevNs > 0 && wallNs > prevNs { } else if prevNs, ok2 := src.lastPktNs[sig.Name]; ok2 && prevNs > 0 && wallNs > prevNs {
// Single packet this tick → gap from the previous tick's packet. // Single packet this tick → gap from the previous tick's packet.
span := float64(n) * float64(1+s.Lost) dtSec = (float64(wallNs) - float64(prevNs)) / 1e9 / float64(n)
dtSec = (float64(wallNs) - float64(prevNs)) / 1e9 / span
} else { } else {
// Truly first packet ever — inter-packet timing unknown. // Truly first packet ever — inter-packet timing unknown.
// Skip to avoid poisoning the ring with wrongly-spaced timestamps; // Skip to avoid poisoning the ring with wrongly-spaced timestamps;
@@ -1,139 +0,0 @@
package wshub
import (
"math"
"testing"
"time"
"marte2/common/udpsprotocol"
)
// pktSignal is a 4-element array with no time signal and no declared sampling
// rate, i.e. the TimeModePacket path where dt has to be inferred from the gap
// between packets.
func pktSignal(name string) udpsprotocol.SignalInfo {
return udpsprotocol.SignalInfo{
Name: name,
TypeCode: 8, // float64
NumDimensions: 1,
NumRows: 4,
NumCols: 1,
TimeMode: udpsprotocol.TimeModePacket,
TimeSignalIdx: udpsprotocol.NoTimeSignal,
}
}
// newPacketDtHub builds a Hub with a ring for one packet-timed array signal and
// returns both. It does not start Run(): buildBinaryDataMessageForSource is
// called directly so the timestamps it produces can be read back verbatim.
func newPacketDtHub(t *testing.T, sigName string) (*Hub, *sourceHubState, *sigRing) {
t.Helper()
h := NewHub()
src := &sourceHubState{
id: "s1",
signals: []udpsprotocol.SignalInfo{pktSignal(sigName)},
timeSigCalib: map[string]float64{},
lastPktNs: map[string]int64{},
lastFrameMeasured: map[string]float64{},
lastFrameEndT: map[string]float64{},
gapEMA: map[string]float64{},
}
rb := newSigRing(4096)
h.rings["s1:"+sigName] = rb
return h, src, rb
}
// packet builds a one-signal batch entry arriving at t0 with the given loss
// count; the values are irrelevant, only the timestamps are under test.
func packet(sigName string, at time.Time, lost uint32, n int) udpsprotocol.DataSample {
vals := make([]float64, n)
return udpsprotocol.DataSample{WallTime: at, Values: map[string][]float64{sigName: vals}, Lost: lost}
}
// ringTimes returns the timestamps written to the ring, in order.
func ringTimes(rb *sigRing) []float64 {
rb.mu.RLock()
defer rb.mu.RUnlock()
out := make([]float64, 0, rb.size)
start := (rb.head - rb.size + rb.cap) % rb.cap
for i := 0; i < rb.size; i++ {
out = append(out, rb.t[(start+i)%rb.cap])
}
return out
}
// A lost packet widens the inter-packet gap without adding elements to the
// packet that follows it. Dividing the gap by that packet's element count
// alone reports a period too long by exactly the number of packets missing,
// which walks the elements past their own end and into the range the next
// packet claims: they collide there, and the span they vacated stays empty.
func TestPacketDtIgnoresLostPacketWidening(t *testing.T) {
const sig = "Wave"
const n = 4
const dt = 1 * time.Millisecond
base := time.Unix(1700000000, 0)
h, src, rb := newPacketDtHub(t, sig)
// One clean packet establishes lastPktNs.
h.buildBinaryDataMessageForSource(src, []udpsprotocol.DataSample{
packet(sig, base, 0, n)})
// The next producer packet is lost, so the one after it arrives a full
// extra batch later and reports Lost=1.
arrival := base.Add(2 * n * dt)
h.buildBinaryDataMessageForSource(src, []udpsprotocol.DataSample{
packet(sig, arrival, 1, n)})
ts := ringTimes(rb)
if len(ts) != n {
t.Fatalf("ring holds %d points, want %d (the first packet is skipped: no gap yet)", len(ts), n)
}
got := ts[1] - ts[0]
if !nearSec(got, dt.Seconds()) {
t.Errorf("dt = %v s, want %v s (the gap spans two batches, not one)", got, dt.Seconds())
}
// Elements run forward from the packet's own arrival, so a doubled dt
// would stretch this batch across two batch periods and into the range the
// next packet claims.
span := ts[len(ts)-1] - ts[0]
if !nearSec(span, float64(n-1)*dt.Seconds()) {
t.Errorf("batch spans %v s, want %v s: it overruns into the next packet's range",
span, float64(n-1)*dt.Seconds())
}
}
// nearSec compares two intervals in seconds. The hub carries timestamps as
// float64 seconds derived from UnixNano, whose spacing near the current epoch
// is a couple of hundred nanoseconds, so exact equality is not available. The
// defect under test moves the period by a factor of two, three orders of
// magnitude outside this tolerance.
func nearSec(got, want float64) bool { return math.Abs(got-want) <= 1e-6 }
// The correction must be driven by the reported loss and nothing else: with no
// packet missing the period still comes straight from the gap, so a producer
// that genuinely slows down is followed rather than second-guessed.
func TestPacketDtFollowsGapWhenNothingIsLost(t *testing.T) {
const sig = "Wave"
const n = 4
base := time.Unix(1700000000, 0)
h, src, rb := newPacketDtHub(t, sig)
h.buildBinaryDataMessageForSource(src, []udpsprotocol.DataSample{
packet(sig, base, 0, n)})
// Same widened gap as the test above, but reported as no loss: the
// producer really is running at half the rate.
slowDt := 2 * time.Millisecond
h.buildBinaryDataMessageForSource(src, []udpsprotocol.DataSample{
packet(sig, base.Add(n*slowDt), 0, n)})
ts := ringTimes(rb)
if len(ts) != n {
t.Fatalf("ring holds %d points, want %d", len(ts), n)
}
if got := ts[1] - ts[0]; !nearSec(got, slowDt.Seconds()) {
t.Errorf("dt = %v s, want %v s: a real rate change must be followed", got, slowDt.Seconds())
}
}
+1 -14
View File
@@ -221,19 +221,6 @@ func ringCoverage(bucket, capacity int) int {
return capacity / 2 * bucket return capacity / 2 * bucket
} }
// captureLagSec is how much further back than the window itself a ring has to
// reach to deliver a capture of it.
//
// A capture is not read out when its last sample arrives but captureMarginSec
// later, and then only on the next push tick — so by the time the window is
// extracted, its oldest sample is that much deeper in the ring. A ring holding
// exactly the window has already overwritten the front of its own capture, which
// is what made every shot at a short window come back missing its head. The
// pre/post split does not enter into it: the harvest is a post-window after the
// trigger and the read reaches a pre-window before it, so the two sum to the
// window whatever the split.
const captureLagSec = captureMarginSec + 1.0/30.0
// activeWindowSec is the timespan the buffers must cover. An armed trigger owns // activeWindowSec is the timespan the buffers must cover. An armed trigger owns
// it: its pre-window has to already be in the ring when the trigger fires or // it: its pre-window has to already be in the ring when the trigger fires or
// there is nothing to back-fill the capture from. Otherwise it is the widest // there is nothing to back-fill the capture from. Otherwise it is the widest
@@ -241,7 +228,7 @@ const captureLagSec = captureMarginSec + 1.0/30.0
func (h *Hub) activeWindowSec() float64 { func (h *Hub) activeWindowSec() float64 {
if h.trigger != nil && h.trigger.Active() { if h.trigger != nil && h.trigger.Active() {
if cfg := h.trigger.Config(); cfg.windowSec > 0 { if cfg := h.trigger.Config(); cfg.windowSec > 0 {
return cfg.windowSec + captureLagSec return cfg.windowSec
} }
} }
widest := 0.0 widest := 0.0
+3 -6
View File
@@ -120,9 +120,7 @@ func TestActiveWindowSecTakesTheWidestClientWindow(t *testing.T) {
} }
// An armed trigger owns the window: its pre-window has to be in the buffer // An armed trigger owns the window: its pre-window has to be in the buffer
// before the trigger fires or the capture has nothing to back-fill from. The // before the trigger fires or the capture has nothing to back-fill from.
// buffers must reach back past the window itself, because the capture is read
// out a margin and a tick after its last sample lands.
func TestActiveWindowSecPrefersTheArmedTrigger(t *testing.T) { func TestActiveWindowSecPrefersTheArmedTrigger(t *testing.T) {
h := NewHub() h := NewHub()
c := &wsClient{} c := &wsClient{}
@@ -130,9 +128,8 @@ func TestActiveWindowSecPrefersTheArmedTrigger(t *testing.T) {
h.clients[c] = true h.clients[c] = true
h.trigger.SetConfig(trigConfig{signalKey: "s1:sig", windowSec: 45, mode: "normal"}) h.trigger.SetConfig(trigConfig{signalKey: "s1:sig", windowSec: 45, mode: "normal"})
if got := h.activeWindowSec(); got != 45+captureLagSec { if got := h.activeWindowSec(); got != 45 {
t.Fatalf("activeWindowSec = %v, want the trigger's 45 plus the %v harvest lag", t.Fatalf("activeWindowSec = %v, want the trigger's 45", got)
got, captureLagSec)
} }
} }
-22
View File
@@ -341,9 +341,6 @@ func (u *UDPClient) runSession() error {
} }
reassembler := udpsprotocol.NewReassembler(2 * time.Second) reassembler := udpsprotocol.NewReassembler(2 * time.Second)
// Per-session: the producer's counter restarts independently of ours, so
// the gate must not carry a counter over from the previous connection.
var gate udpsprotocol.SequenceGate
buf := make([]byte, readBufSize) buf := make([]byte, readBufSize)
var currentSigs []udpsprotocol.SignalInfo var currentSigs []udpsprotocol.SignalInfo
var currentPublishMode uint8 var currentPublishMode uint8
@@ -417,20 +414,11 @@ func (u *UDPClient) runSession() error {
if len(currentSigs) == 0 { if len(currentSigs) == 0 {
continue continue
} }
fresh, lost := gate.Accept(hdr.Counter)
if !fresh {
continue
}
samples, err := udpsprotocol.ParseData(complete, currentSigs, currentPublishMode, arrivalTime) samples, err := udpsprotocol.ParseData(complete, currentSigs, currentPublishMode, arrivalTime)
if err != nil { if err != nil {
log.Printf("[%s] udp: parse data: %v", u.sourceID, err) log.Printf("[%s] udp: parse data: %v", u.sourceID, err)
continue continue
} }
// The gap precedes the packet, so it belongs to its first slot only;
// the slots after it are consecutive cycles of the same batch.
if len(samples) > 0 {
samples[0].Lost = lost
}
for _, s := range samples { for _, s := range samples {
u.hub.PushDataForSource(u.sourceID, s) u.hub.PushDataForSource(u.sourceID, s)
} }
@@ -601,9 +589,6 @@ func (u *UDPClient) runMulticastSession() error {
}() }()
reassembler := udpsprotocol.NewReassembler(2 * time.Second) reassembler := udpsprotocol.NewReassembler(2 * time.Second)
// Per-session, as in runSession(): a counter from the previous connection
// would reject the whole new stream.
var gate udpsprotocol.SequenceGate
buf := make([]byte, readBufSize) buf := make([]byte, readBufSize)
for { for {
@@ -644,18 +629,11 @@ func (u *UDPClient) runMulticastSession() error {
if len(currentSigs) == 0 { if len(currentSigs) == 0 {
continue continue
} }
fresh, lost := gate.Accept(hdr.Counter)
if !fresh {
continue
}
samples, parseErr := udpsprotocol.ParseData(complete, currentSigs, currentPublishMode, arrivalTime) samples, parseErr := udpsprotocol.ParseData(complete, currentSigs, currentPublishMode, arrivalTime)
if parseErr != nil { if parseErr != nil {
log.Printf("[%s] multicast: parse data: %v", u.sourceID, parseErr) log.Printf("[%s] multicast: parse data: %v", u.sourceID, parseErr)
continue continue
} }
if len(samples) > 0 {
samples[0].Lost = lost
}
for _, s := range samples { for _, s := range samples {
u.hub.PushDataForSource(u.sourceID, s) u.hub.PushDataForSource(u.sourceID, s)
} }
+13 -119
View File
@@ -92,14 +92,6 @@ type triggerEngine struct {
bufGrowth float64 bufGrowth float64
bufKnown bool bufKnown bool
bufRateOK bool bufRateOK bool
// bufCoverage is the maximum span (seconds) the ring can reach at its
// current bucket and capacity — the gate must never demand more than this,
// or a ring whose coverage is below the window can never satisfy it. 0 =
// unknown (no measurable rate).
bufCoverage float64
// bufArchived is true when the disk history already spans the trigger
// window, so a short capture's front can be back-filled from it.
bufArchived bool
// Reference point the growth is measured against. // Reference point the growth is measured against.
bufRefSpan, bufRefWall float64 bufRefSpan, bufRefWall float64
@@ -116,22 +108,6 @@ type triggerEngine struct {
firedPost float64 firedPost float64
firedValid bool firedValid bool
// The edge to fire on as soon as the FSM rearms, in sample time. Recorded
// while a capture is still being collected or handed out, for edges late
// enough that a capture of them would not overlap the one in flight.
//
// Without this the trigger is deaf from its own trigger point until the
// capture has been harvested — a post-window plus captureMarginSec — and
// then for the holdoff on top of that, and afterwards waits for a FRESH
// edge. On a sparse pulse train that rounds the capture spacing up to a
// whole pulse period: at the default 1 s window the blind stretch comes to
// 1.15 s, so a 1 Hz train was caught at 0.5 Hz and a wider window lost whole
// multiples. Remembering the edge instead makes the blind stretch exactly
// the post-window it has to be, since the capture is built from the edge's
// own timestamp and the ring still holds everything around it.
pendingT float64
pendingValid bool
rearmAt float64 // wall-clock seconds; 0 when no rearm is pending rearmAt float64 // wall-clock seconds; 0 when no rearm is pending
} }
@@ -187,14 +163,10 @@ func (te *triggerEngine) SetConfig(cfg trigConfig) {
if base != te.baseKey { if base != te.baseKey {
// The buffer measurement belongs to the old signal's ring. // The buffer measurement belongs to the old signal's ring.
te.bufKnown, te.bufRateOK = false, false te.bufKnown, te.bufRateOK = false, false
te.bufCoverage, te.bufArchived = 0, false
} }
te.baseKey, te.elemIdx = base, idx te.baseKey, te.elemIdx = base, idx
te.prevValid = false te.prevValid = false
te.prevValue = 0 te.prevValue = 0
// An edge held over from the old configuration would be latched against the
// new window, whose fill the gate has not vouched for.
te.pendingValid = false
} }
func (te *triggerEngine) Config() trigConfig { func (te *triggerEngine) Config() trigConfig {
@@ -203,37 +175,15 @@ func (te *triggerEngine) Config() trigConfig {
return te.cfg return te.cfg
} }
// Arm starts a fresh acquisition. It is the user's own arm, so it discards any
// edge remembered during the previous capture: the user asked for the next
// event, not for one that has already been and gone.
func (te *triggerEngine) Arm() { func (te *triggerEngine) Arm() {
te.mu.Lock() te.mu.Lock()
te.state = trigArmed te.state = trigArmed
te.prevValid = false te.prevValid = false
te.prevValue = 0 te.prevValue = 0
te.pendingValid = false
te.rearmAt = 0 te.rearmAt = 0
te.mu.Unlock() te.mu.Unlock()
} }
// rearm is the automatic arm at the end of a capture. Unlike Arm it honours an
// edge that arrived while the capture was being collected, firing on it at once
// rather than waiting for the next one — see pendingT. It also keeps the level
// tracked through the dead time, so the first sample after rearming is compared
// against its real predecessor instead of being spent seeding one.
func (te *triggerEngine) rearm() {
te.mu.Lock()
te.rearmAt = 0
if te.pendingValid {
t := te.pendingT
te.pendingValid = false
te.latchWindowLocked(t)
} else {
te.state = trigArmed
}
te.mu.Unlock()
}
func (te *triggerEngine) Disarm() { func (te *triggerEngine) Disarm() {
te.mu.Lock() te.mu.Lock()
te.state = trigIdle te.state = trigIdle
@@ -241,7 +191,6 @@ func (te *triggerEngine) Disarm() {
te.prevValid = false te.prevValid = false
te.prevValue = 0 te.prevValue = 0
te.firedValid = false te.firedValid = false
te.pendingValid = false
te.rearmAt = 0 te.rearmAt = 0
te.mu.Unlock() te.mu.Unlock()
} }
@@ -294,16 +243,13 @@ const bufGrowthIntervalSec = 0.5
const bufGrowthSmooth = 0.5 const bufGrowthSmooth = 0.5
// setBuffered records how far back the trigger signal's ring reaches, at wall // setBuffered records how far back the trigger signal's ring reaches, at wall
// clock now, and derives how fast that is growing. coverage is the maximum // clock now, and derives how fast that is growing. Pass known=false when there
// span (seconds) the ring can reach at its current bucket/capacity; archived // is no such ring.
// says the disk history already spans the trigger window. Pass known=false when func (te *triggerEngine) setBuffered(span float64, known bool, now float64) {
// there is no ring to measure.
func (te *triggerEngine) setBuffered(span, coverage float64, archived, known bool, now float64) {
te.mu.Lock() te.mu.Lock()
defer te.mu.Unlock() defer te.mu.Unlock()
if !known { if !known {
te.bufKnown, te.bufRateOK = false, false te.bufKnown, te.bufRateOK = false, false
te.bufCoverage, te.bufArchived = 0, false
return return
} }
if !te.bufKnown { if !te.bufKnown {
@@ -311,8 +257,6 @@ func (te *triggerEngine) setBuffered(span, coverage float64, archived, known boo
te.bufRefSpan, te.bufRefWall = span, now te.bufRefSpan, te.bufRefWall = span, now
} }
te.bufSpan = span te.bufSpan = span
te.bufCoverage = coverage
te.bufArchived = archived
dt := now - te.bufRefWall dt := now - te.bufRefWall
if dt < bufGrowthIntervalSec { if dt < bufGrowthIntervalSec {
return return
@@ -350,17 +294,9 @@ func (te *triggerEngine) setBuffered(span, coverage float64, archived, known boo
// anyway. A full one grows only as fast as its incoming samples free space — // anyway. A full one grows only as fast as its incoming samples free space —
// re-bucketing to a longer window replaces dense old samples with sparse new // re-bucketing to a longer window replaces dense old samples with sparse new
// ones — and it is that case, growth well below 1, where firing on the // ones — and it is that case, growth well below 1, where firing on the
// // pre-window alone delivers a capture whose front has been overwritten by the
// Two escapes keep an armed trigger from staying deaf forever: // time it is read. In the steady state growth is 0 and need is the whole
// // window, which a ring tuned for that window already exceeds, so nothing waits.
// - archived — the disk history already spans the window, so the front of a
// capture can be back-filled from it; the ring only needs to
// hold the pre-window worth of recent data.
// - coverage — never demand more than the ring can physically reach. If its
// coverage saturates below the window (a measured source rate
// that over-estimates the true one), the gate opens once the
// ring is full anyway and a short capture is delivered instead
// of deafness.
func (te *triggerEngine) fillNeedLocked() float64 { func (te *triggerEngine) fillNeedLocked() float64 {
pre := te.cfg.windowSec * te.cfg.prePercent / 100 pre := te.cfg.windowSec * te.cfg.prePercent / 100
growth := 0.0 // until measured, assume the buffer will not fill on its own growth := 0.0 // until measured, assume the buffer will not fill on its own
@@ -371,14 +307,6 @@ func (te *triggerEngine) fillNeedLocked() float64 {
if need < pre { if need < pre {
need = pre need = pre
} }
if te.bufArchived {
// The archive back-fills the front; the ring holds the post-trigger
// window live, so the pre-window is all it needs to have reached.
return pre
}
if te.bufCoverage > 0 && need > te.bufCoverage {
need = te.bufCoverage
}
return need return need
} }
@@ -438,11 +366,7 @@ func (te *triggerEngine) feed(key string, nElem int, t, v []float64) {
te.lastT = t[len(t)-1] te.lastT = t[len(t)-1]
te.lastTOK = true te.lastTOK = true
te.lastFeedWall = float64(time.Now().UnixNano()) / 1e9 te.lastFeedWall = float64(time.Now().UnixNano()) / 1e9
if te.state != trigArmed {
// A capture in flight does not stop the comparator; it only changes what an
// edge does. See pendingT.
inFlight := te.state == trigCollecting || te.state == trigTriggered
if te.state != trigArmed && !inFlight {
return return
} }
step, start := 1, 0 step, start := 1, 0
@@ -457,20 +381,12 @@ func (te *triggerEngine) feed(key string, nElem int, t, v []float64) {
// which is what made the first shot after a window change come back short. // which is what made the first shot after a window change come back short.
// Track the level meanwhile, so the first edge once the buffer is deep // Track the level meanwhile, so the first edge once the buffer is deep
// enough is still measured against the right previous sample. // enough is still measured against the right previous sample.
if !inFlight && te.fillLocked() < 1 { if te.fillLocked() < 1 {
for i := start; i < len(v); i += step { for i := start; i < len(v); i += step {
te.prevValue, te.prevValid = v[i], true te.prevValue, te.prevValid = v[i], true
} }
return return
} }
// The earliest trigger point a new capture may take. The one in flight owns
// everything up to the end of its own post-window, and the holdoff — a guard
// against re-triggering on the ringing of the SAME event — is measured from
// its trigger point too, so the two overlap rather than add.
notBefore := math.Inf(1)
if inFlight && te.firedValid {
notBefore = te.trigTime + math.Max(te.firedPost, te.cfg.holdoffSec)
}
thr := te.cfg.threshold thr := te.cfg.threshold
for i := start; i < len(t); i += step { for i := start; i < len(t); i += step {
if !te.prevValid { if !te.prevValid {
@@ -490,19 +406,10 @@ func (te *triggerEngine) feed(key string, nElem int, t, v []float64) {
default: default:
fired = up fired = up
} }
if !fired { if fired {
continue
}
if !inFlight {
te.latchWindowLocked(t[i]) te.latchWindowLocked(t[i])
return return
} }
// Keep the FIRST qualifying edge and go on tracking the level: a later
// one would be no more use, and stopping here would leave prevValue
// stale by the time the FSM rearms.
if !te.pendingValid && t[i] >= notBefore {
te.pendingT, te.pendingValid = t[i], true
}
} }
} }
@@ -685,32 +592,19 @@ func (h *Hub) refreshTriggerFill() {
return return
} }
now := float64(time.Now().UnixNano()) / 1e9 now := float64(time.Now().UnixNano()) / 1e9
key := h.trigger.baseSignalKey()
var rb *sigRing var rb *sigRing
if key != "" { if key := h.trigger.baseSignalKey(); key != "" {
rb = h.getRing(key) rb = h.getRing(key)
} }
if rb == nil { if rb == nil {
// Nothing to measure. Do not gate on a signal the hub does not carry: // Nothing to measure. Do not gate on a signal the hub does not carry:
// that would leave the trigger armed forever, which is worse than a // that would leave the trigger armed forever, which is worse than a
// short capture. // short capture.
h.trigger.setBuffered(0, 0, false, false, now) h.trigger.setBuffered(0, false, now)
return return
} }
_, span := rb.stats() _, span := rb.stats()
// Maximum span the ring can ever reach at its current bucket/capacity, in h.trigger.setBuffered(span, true, now)
// seconds. The gate must never demand more than this, or a ring whose
// coverage is below the window (a measured source rate that over-estimates
// the true one) can never satisfy it.
coverage := 0.0
if rate := rb.sourceRate(); rate > 0 {
coverage = float64(ringCoverage(rb.bucketSize(), rb.capacity())) / rate
}
// If the disk archive already spans the trigger window, the front of a
// short capture can be back-filled from it, so the ring need not cover the
// whole window on its own.
archived := h.hist.coversWindow(key, h.trigger.Config().windowSec)
h.trigger.setBuffered(span, coverage, archived, true, now)
} }
// triggerTick services the trigger FSM; called from Hub.Run() on every push tick. // triggerTick services the trigger FSM; called from Hub.Run() on every push tick.
@@ -746,7 +640,7 @@ func (h *Hub) triggerTick() {
// file of its own, where nothing overwrites it until the next trigger. // file of its own, where nothing overwrites it until the next trigger.
h.hist.captureRange(trigTime-pre, trigTime+post) h.hist.captureRange(trigTime-pre, trigTime+post)
} else if h.trigger.dueRearm(nowSec) { } else if h.trigger.dueRearm(nowSec) {
h.trigger.rearm() h.trigger.Arm()
} }
if h.trigger.stateUnsent() { if h.trigger.stateUnsent() {
@@ -1,367 +0,0 @@
package wshub
import (
"encoding/binary"
"math"
"testing"
)
/*
Sporadic-signal trigger coverage.
Every other trigger test in this package feeds a periodic waveform, or a
hand-built two-sample batch. Neither can show a trigger that is blind most of
the time: a sine crosses the threshold again a few milliseconds after every
missed crossing, so a trigger losing 80 % of its edges still fires steadily and
looks healthy. A sparse train — 0000000111000000000000, one short burst in a
long flat run — has nothing to fall back on, so every missed edge is a missed
capture and the yield is a direct measure of how long the FSM was deaf.
That deafness is what these tests pin down. It is not a bug in itself: a capture
cannot be harvested before the samples after its trigger point exist, so the
trigger is necessarily blind for its own post-trigger window. What must NOT
happen is for edges arriving after that window to be thrown away as well.
*/
// pulseTrainSim drives a Hub the way Run() does — ingest on one side, the
// trigger tick on the other — on a simulated clock.
type pulseTrainSim struct {
rateHz float64
batchSec float64
pulsePeriod float64
pulseSamples int
simSec float64
windowSec float64
prePercent float64
holdoffSec float64
armAt float64
// When windowChangeAt > 0 the window is switched to windowChangeTo at that
// time and the trigger re-armed, as a user editing the trigger bar would.
windowChangeAt float64
windowChangeTo float64
}
type pulseTrainResult struct {
pulses int // pulse starts presented after the trigger was armed
shots int // captures actually delivered
trigTimes []float64 // the sample time each capture triggered on
worstCov float64 // smallest fraction of its window a capture spanned
holdDeclined int // captures the zoom hold would not answer for
drawnPulses int // pulses visible in the delivered frames
wantPulses int // pulses those frames' windows really contained
gatedPulses int // pulses that arrived armed but with the fill gate shut
}
// yield is the fraction of presented pulses that produced a capture.
func (r pulseTrainResult) yield() float64 {
if r.pulses == 0 {
return 0
}
return float64(r.shots) / float64(r.pulses)
}
// run executes the simulation and returns what the trigger caught.
func (s pulseTrainSim) run(t *testing.T, key string) pulseTrainResult {
t.Helper()
h := NewHub()
h.rings[key] = newSigRing(ringCapInitial)
h.trigger.SetConfig(trigConfig{
signalKey: key, edge: "rising", threshold: 0.5,
windowSec: s.windowSec, prePercent: s.prePercent,
mode: "normal", holdoffSec: s.holdoffSec,
})
res := pulseTrainResult{worstCov: 1}
nBatch := int(s.rateHz * s.batchSec)
ts := make([]float64, nBatch)
vs := make([]float64, nBatch)
armed, changed := false, false
for now := 0.0; now < s.simSec; now += s.batchSec {
nPulseStarts := 0
for i := range ts {
ts[i] = now + float64(i)/s.rateHz
// Position within the current pulse period, in samples.
k := int((ts[i] - math.Floor(ts[i]/s.pulsePeriod)*s.pulsePeriod) * s.rateHz)
if k < s.pulseSamples {
vs[i] = 1
if k == 0 {
nPulseStarts++
}
} else {
vs[i] = 0
}
}
if !armed && now >= s.armAt {
h.trigger.Arm()
armed = true
}
if s.windowChangeAt > 0 && !changed && now >= s.windowChangeAt {
cfg := h.trigger.Config()
cfg.windowSec = s.windowChangeTo
h.trigger.SetConfig(cfg)
h.trigger.Arm()
changed = true
}
if armed {
res.pulses += nPulseStarts
if nPulseStarts > 0 && h.trigger.State() == trigArmed {
h.trigger.mu.Lock()
f := h.trigger.fillLocked()
h.trigger.mu.Unlock()
if f < 1 {
res.gatedPulses += nPulseStarts
}
}
}
h.ingest(key, 1, ts, vs)
// Mirror triggerTick, on the simulated clock.
tick := now + s.batchSec
h.retuneRings(tick)
_, span := h.rings[key].stats()
h.trigger.setBuffered(span, 0, false, true, tick)
if trigTime, pre, post, ok := h.trigger.dueCapture(tick); ok {
if buf := h.buildTriggerCapture(trigTime, pre, post); buf != nil {
res.shots++
res.trigTimes = append(res.trigTimes, trigTime)
first, last, _ := decodeCaptureSpan(t, buf, key)
if cov := (last - first) / (pre + post); cov < res.worstCov {
res.worstCov = cov
}
if _, _, ok := h.capture.slice(key, trigTime-pre, trigTime+post); !ok {
res.holdDeclined++
}
// What the client would actually draw, against what the window
// really contained. A wide window holds several pulses, and a
// frame showing only the one it triggered on has lost the rest
// between the ring, the bucketing and the decimation.
_, fv := decodeCaptureSig(t, buf, key)
res.drawnPulses += countPulses(fv, 0.5)
res.wantPulses += countPulseStarts(trigTime-pre, trigTime+post,
s.pulsePeriod, 1/s.rateHz)
}
h.trigger.markTriggered(tick)
} else if h.trigger.dueRearm(tick) {
h.trigger.rearm()
}
}
return res
}
// decodeCaptureSig pulls one signal's samples out of a v2 capture frame.
func decodeCaptureSig(t *testing.T, buf []byte, key string) (ts, vs []float64) {
t.Helper()
off := 1 + 8 + 8 + 8
nSig := int(binary.LittleEndian.Uint32(buf[off:]))
off += 4
for i := 0; i < nSig; i++ {
kl := int(binary.LittleEndian.Uint16(buf[off:]))
off += 2
k := string(buf[off : off+kl])
off += kl
cnt := int(binary.LittleEndian.Uint32(buf[off:]))
off += 4
if k == key {
ts = make([]float64, cnt)
vs = make([]float64, cnt)
for j := 0; j < cnt; j++ {
ts[j] = math.Float64frombits(binary.LittleEndian.Uint64(buf[off+j*8:]))
vs[j] = math.Float64frombits(binary.LittleEndian.Uint64(buf[off+cnt*8+j*8:]))
}
}
off += cnt * 16
}
return
}
// countPulses counts runs of samples at or above thr.
func countPulses(v []float64, thr float64) int {
n, in := 0, false
for _, x := range v {
if x >= thr {
if !in {
n, in = n+1, true
}
} else {
in = false
}
}
return n
}
// countPulseStarts is how many pulse starts fall inside [t0, t1]. A pulse
// starting within one sample of t1 is not counted: only its first sample is
// inside the window, and the ring's min/max bucket for it may put that sample's
// extremum just past the edge, which is a boundary artefact rather than a loss.
func countPulseStarts(t0, t1, period, dt float64) int {
n := 0
for k := math.Floor(t0 / period); k*period <= t1; k++ {
if p := k * period; p >= t0 && p < t1-2*dt {
n++
}
}
return n
}
// deadTimeSec is how long the FSM is blind after firing at t: it must acquire
// the post-trigger window before the capture can be harvested, and the holdoff
// guards against re-triggering on the same event. Both are measured from the
// trigger point, so they overlap rather than add.
func deadTimeSec(windowSec, prePercent, holdoffSec float64) float64 {
return math.Max(windowSec*(1-prePercent/100), holdoffSec)
}
// A trigger cannot show two windows at once, so pulses closer together than its
// post-trigger window are necessarily lost. Pulses spaced FURTHER apart than
// that are not: nothing about the acquisition prevents catching every one.
//
// This is the reported failure. The FSM used to go deaf from the trigger point
// until the capture had been harvested (a post-window plus captureMarginSec)
// and the holdoff had then elapsed on top of that, then wait for a fresh edge —
// so the effective spacing was rounded UP to a whole pulse period. At the
// default 1 s window and 0.2 s holdoff the blind stretch came to 1.15 s, which
// is longer than a 1 s pulse period by a hair, and a pulse train at 1 Hz was
// caught at 0.5 Hz. Widening the window made it worse in whole multiples.
func TestSporadicPulsesWiderThanThePostWindowAreAllCaught(t *testing.T) {
const key = "s1:Ch1"
cases := []struct{ window, period float64 }{
{0.5, 0.5}, // post 0.4 s
{1.0, 1.0}, // post 0.8 s — the case the report was made against
{2.0, 2.0}, // post 1.6 s
{5.0, 5.0}, // post 4.0 s
}
for _, c := range cases {
sim := pulseTrainSim{
rateHz: 1000, batchSec: 1.0 / 30.0,
pulsePeriod: c.period, pulseSamples: 3,
simSec: 41 * c.period, windowSec: c.window, prePercent: 20,
holdoffSec: autoRearmDelaySec, armAt: c.period,
}
res := sim.run(t, key)
// Two pulses are always in flight rather than caught: the one that lands
// as the trigger arms, and the one still being collected when the run
// ends.
if got := res.yield(); got < 0.94 {
t.Errorf("window %.1f s, pulse every %.1f s: caught %d of %d (%.0f%%); "+
"the post-trigger window is only %.2f s, so every pulse fits",
c.window, c.period, res.shots, res.pulses, 100*got,
c.window*0.8)
}
}
}
// The loss that remains must be the loss that has to remain. A capture cannot
// start before the previous one's post-window is acquired, and it can only start
// on a pulse, so consecutive captures are a dead time apart rounded UP to the
// next pulse — never further. Any longer gap means an edge that the acquisition
// no longer needed was thrown away anyway.
//
// The bound is stated as dead + period rather than ceil(dead/period)*period
// because when the two divide exactly, whether the pulse at the boundary counts
// comes down to the last bit of the sample timestamp. Both answers are correct;
// a gap beyond either is not.
func TestSporadicCaptureGapsStayWithinTheDeadTime(t *testing.T) {
const key = "s1:Ch1"
for _, window := range []float64{0.5, 1.0, 2.0, 5.0} {
for _, period := range []float64{0.25, 0.5, 1.0, 2.0} {
sim := pulseTrainSim{
rateHz: 1000, batchSec: 1.0 / 30.0,
pulsePeriod: period, pulseSamples: 3,
simSec: 60, windowSec: window, prePercent: 20,
holdoffSec: autoRearmDelaySec, armAt: 1.0,
}
res := sim.run(t, key)
dead := deadTimeSec(window, 20, autoRearmDelaySec)
limit := dead + period + 2*sim.batchSec
worst, worstAt := 0.0, 0.0
for i := 1; i < len(res.trigTimes); i++ {
if g := res.trigTimes[i] - res.trigTimes[i-1]; g > worst {
worst, worstAt = g, res.trigTimes[i-1]
}
}
if worst > limit {
t.Errorf("window %.1f s, pulse every %.2f s: %.2f s between the captures "+
"at %.2f s and %.2f s; the dead time is only %.2f s, so %.2f s is the most "+
"that can be missed",
window, period, worst, worstAt, worstAt+worst, dead, limit)
}
t.Logf("window %.1f s, pulse every %.2f s: %d/%d captures (%.0f%%), "+
"dead time %.2f s, worst gap %.2f s, gated %d",
window, period, res.shots, res.pulses, 100*res.yield(), dead,
worst, res.gatedPulses)
}
}
}
// Whatever the trigger does catch has to come back whole: a window wide enough
// to hold several pulses must show all of them, at every rate, including the
// rates that force the ring into min/max bucketing.
func TestSporadicCaptureShowsEveryPulseInItsWindow(t *testing.T) {
const key = "s1:Ch1"
for _, rate := range []float64{1000, 200e3} {
for _, window := range []float64{1.0, 2.0, 5.0} {
sim := pulseTrainSim{
rateHz: rate, batchSec: 1.0 / 30.0,
pulsePeriod: 0.5, pulseSamples: 3,
simSec: 40, windowSec: window, prePercent: 20,
holdoffSec: autoRearmDelaySec, armAt: 1.0,
}
res := sim.run(t, key)
if res.shots == 0 {
t.Fatalf("rate %.0f window %.1f s: no captures at all", rate, window)
}
// wantPulses excludes the pulse straddling each window's far edge,
// whose bucket may place its extremum just past it, so the frames
// may legitimately draw a few more than that — but never fewer.
if res.drawnPulses < res.wantPulses {
t.Errorf("rate %.0f window %.1f s: frames drew %d pulses, their windows held %d",
rate, window, res.drawnPulses, res.wantPulses)
}
if res.worstCov < 0.98 {
t.Errorf("rate %.0f window %.1f s: worst capture spanned %.0f%% of its window",
rate, window, 100*res.worstCov)
}
if res.holdDeclined > 0 {
t.Errorf("rate %.0f window %.1f s: the zoom hold declined %d of %d captures",
rate, window, res.holdDeclined, res.shots)
}
}
}
}
// Widening the window mid-run is the gesture the report came from. The fill gate
// holds the first shot off until the ring reaches back far enough, which is
// correct; what it must not do is stay shut, nor leave the trigger losing pulses
// once the ring has caught up.
func TestSporadicYieldRecoversAfterAWindowChange(t *testing.T) {
const key = "s1:Ch1"
for _, w := range []float64{1.0, 2.0, 5.0} {
sim := pulseTrainSim{
rateHz: 200e3, batchSec: 1.0 / 30.0,
pulsePeriod: w, pulseSamples: 3,
simSec: 30 * w, windowSec: 0.2, prePercent: 20,
holdoffSec: autoRearmDelaySec, armAt: 1.0,
windowChangeAt: 10 * w, windowChangeTo: w,
}
res := sim.run(t, key)
// Count only what happened after the change settled.
after, want := 0, 0
for _, tt := range res.trigTimes {
if tt > 11*w {
after++
}
}
for p := 11 * w; p < 30*w; p += w {
want++
}
if float64(after) < 0.9*float64(want) {
t.Errorf("window 0.2 -> %.1f s: %d captures in the %d pulses after the change",
w, after, want)
}
}
}
+6 -58
View File
@@ -297,9 +297,9 @@ func TestCollectingIsBroadcast(t *testing.T) {
// later. It forgets any earlier measurement first, so the rate is the one // later. It forgets any earlier measurement first, so the rate is the one
// asked for rather than a blend with it. // asked for rather than a blend with it.
func setFill(te *triggerEngine, span, growth, now float64) { func setFill(te *triggerEngine, span, growth, now float64) {
te.setBuffered(0, 0, false, false, now) te.setBuffered(0, false, now)
te.setBuffered(span-growth, 0, false, true, now) te.setBuffered(span-growth, true, now)
te.setBuffered(span, 0, false, true, now+1) te.setBuffered(span, true, now+1)
} }
// What has to hold is that the buffer spans the whole window by the time the // What has to hold is that the buffer spans the whole window by the time the
@@ -417,9 +417,9 @@ func TestForceIgnoresFillGate(t *testing.T) {
// interval, so they refresh the span and leave the seeded rate alone. // interval, so they refresh the span and leave the seeded rate alone.
func seedFillNow(te *triggerEngine, span, growth float64) { func seedFillNow(te *triggerEngine, span, growth float64) {
now := float64(time.Now().UnixNano()) / 1e9 now := float64(time.Now().UnixNano()) / 1e9
te.setBuffered(0, 0, false, false, now-1) te.setBuffered(0, false, now-1)
te.setBuffered(span-growth, 0, false, true, now-1) te.setBuffered(span-growth, true, now-1)
te.setBuffered(span, 0, false, true, now) te.setBuffered(span, true, now)
} }
// While it holds off, the trigger looks identical to one that is ignoring // While it holds off, the trigger looks identical to one that is ignoring
@@ -505,55 +505,3 @@ func drainStates(t *testing.T, h *Hub) []map[string]any {
} }
} }
} }
// A ring whose coverage saturates below the window (measured source rate that
// over-estimates the true one) can never satisfy the full-window need. The
// coverage clamp must open the gate once the ring is full, delivering a short
// capture rather than staying deaf forever.
func TestFillNeedClampedToCoverage(t *testing.T) {
te := newTriggerEngine()
te.SetConfig(trigConfig{signalKey: "s:x", windowSec: 60, prePercent: 20, mode: "normal", holdoffSec: 0.2})
setFill(te, 50, 0, 100) // ring full at 50 s, no growth
te.mu.Lock()
te.bufCoverage = 50 // the ring can never reach further back
te.mu.Unlock()
if need := te.fillNeedLocked(); need != 50 {
t.Errorf("need = %v, want 50 (clamped to coverage, not the 60 s window)", need)
}
if f := te.fillLocked(); f < 1 {
t.Errorf("fillLocked = %v, want >= 1: a full ring below the window must still open the gate", f)
}
// Without the clamp the gate would stay shut forever.
te.mu.Lock()
te.bufCoverage = 0
te.mu.Unlock()
if f := te.fillLocked(); f >= 1 {
t.Errorf("baseline: fillLocked = %v, want < 1 without a coverage clamp", f)
}
}
// When the disk archive already spans the window it can back-fill the front of
// a capture, so the gate must only require the ring to have reached the
// pre-window, not the whole window.
func TestFillNeedArchiveLowersToPreWindow(t *testing.T) {
te := newTriggerEngine()
te.SetConfig(trigConfig{signalKey: "s:x", windowSec: 60, prePercent: 20, mode: "normal", holdoffSec: 0.2})
setFill(te, 30, 0, 100) // ring holds only 30 s, no growth → need 60 without archive
te.mu.Lock()
te.bufArchived = true
te.mu.Unlock()
if want := 12.0; te.fillNeedLocked() != want { // 60 * 0.20
t.Errorf("need = %v, want %v (archive lowers to the pre-window)", te.fillNeedLocked(), want)
}
// A ring holding just the pre-window opens the gate once archived.
te.mu.Lock()
te.bufSpan = 12
te.mu.Unlock()
if f := te.fillLocked(); f < 1 {
t.Errorf("fillLocked = %v, want >= 1 with pre-window buffered and the archive available", f)
}
}
+4 -33
View File
@@ -22,23 +22,16 @@
* CONFIG payload: * CONFIG payload:
* [uint32 numSigs] * [uint32 numSigs]
* numSigs × UDPSSignalDescriptor (136 bytes each, packed) * numSigs × UDPSSignalDescriptor (136 bytes each, packed)
* [uint8 publishMode] (PublishModeStrict / Accumulate / Decimate) * [uint8 publishMode] (PublishModeStrict / Accumulate / Decimate)
* [uint64 hrtFrequency] ticks per second of the producer's HRT
*
* Everything after the descriptors is an optional trailer: a receiver must
* accept a payload that stops early and must ignore bytes it does not know.
* publishMode defaults to Strict when absent, hrtFrequency to
* UDPS_HRT_FREQUENCY_UNKNOWN.
* *
* DATA payload (Strict / Decimate): * DATA payload (Strict / Decimate):
* [uint64 HRT timestamp] * [uint64 HRT timestamp]
* per-signal data in CONFIG order (quantised or raw, no padding) * per-signal data in CONFIG order (quantised or raw, no padding)
* *
* DATA payload (Accumulate): * DATA payload (Accumulate):
* [uint64 HRT timestamp of the first slot in the batch] * [uint64 HRT timestamp]
* [uint32 numSamples] RT cycles accumulated into this packet * [uint32 numSamples]
* for each signal, in CONFIG order: numSamples × NumElements values * for each signal: if scalar → numSamples elements; else → NumElements once
* (signal-major, one full snapshot per accumulated cycle)
*/ */
#ifndef UDPS_PROTOCOL_H_ #ifndef UDPS_PROTOCOL_H_
@@ -61,14 +54,6 @@ static const uint8 UDPS_TYPE_CONFIG = 1u; ///< Server → Client: signal con
static const uint8 UDPS_TYPE_ACK = 2u; ///< Client → Server: ack counter static const uint8 UDPS_TYPE_ACK = 2u; ///< Client → Server: ack counter
static const uint8 UDPS_TYPE_CONNECT = 3u; ///< Client → Server: connect request static const uint8 UDPS_TYPE_CONNECT = 3u; ///< Client → Server: connect request
static const uint8 UDPS_TYPE_DISCONNECT = 4u; ///< Client → Server: disconnect static const uint8 UDPS_TYPE_DISCONNECT = 4u; ///< Client → Server: disconnect
static const uint8 UDPS_TYPE_SUBSCRIBE = 5u; ///< Client → Server (TCP stream service only): payload = stream name(s), comma separated
/** TCP stream service: most streams one connection may multiplex. On such a
* connection every server frame carries the stream's position in the SUBSCRIBE
* list in the header's fragmentIdx field (totalFragments stays 1). */
static const uint32 UDPS_TCP_MAX_STREAMS = 64u;
/** TCP stream service: longest SUBSCRIBE payload (comma separated names, each <= 255). */
static const uint32 UDPS_TCP_MAX_SUBSCRIBE_PAYLOAD = 4096u;
/*---------------------------------------------------------------------------*/ /*---------------------------------------------------------------------------*/
/* Wire packet header (17 bytes) */ /* Wire packet header (17 bytes) */
@@ -138,20 +123,6 @@ static const uint8 UDPS_PUBLISH_STRICT = 0u; ///< One packet per Synchronise
static const uint8 UDPS_PUBLISH_ACCUMULATE = 1u; ///< Variable batch; flush on size or time static const uint8 UDPS_PUBLISH_ACCUMULATE = 1u; ///< Variable batch; flush on size or time
static const uint8 UDPS_PUBLISH_DECIMATE = 2u; ///< One packet per Ratio calls static const uint8 UDPS_PUBLISH_DECIMATE = 2u; ///< One packet per Ratio calls
/*---------------------------------------------------------------------------*/
/* HRT frequency (CONFIG trailing uint64) */
/*---------------------------------------------------------------------------*/
/**
* Sentinel for a CONFIG that carries no HRT frequency, either because the
* trailer is absent (producer older than this field) or because the producer
* could not determine it. DATA timestamps are raw ticks of the producer's
* high-resolution timer, so without this a receiver on another host has no
* way to turn them into seconds and can only fall back to its own timer's
* frequency — which is right only while the two happen to agree.
*/
static const uint64 UDPS_HRT_FREQUENCY_UNKNOWN = 0u;
/*---------------------------------------------------------------------------*/ /*---------------------------------------------------------------------------*/
/* CONFIG payload — per-signal descriptor */ /* CONFIG payload — per-signal descriptor */
/*---------------------------------------------------------------------------*/ /*---------------------------------------------------------------------------*/
-5
View File
@@ -63,11 +63,6 @@ Full streaming pipeline: MARTe2 (FileReader -> IOGAM -> UDPStreamer) -> StreamHu
(live, zoom, window, trigger). The validator compares the recording against the analytic (live, zoom, window, trigger). The validator compares the recording against the analytic
ground truth (fidelity, sine shape fit, continuity) and optionally a fed-reference tap. ground truth (fidelity, sine shape fit, continuity) and optionally a fed-reference tap.
Chain scenarios `s60`-`s73` use TCP transports (`network = "tcp"` or `"tcp_service"`). The
optional `hub` field adds `{"mode": "mux"}` (the hub takes all sources over one multiplexed
connection, `s72`) or `{"mode": "join"}` (a `TCPStreamJoin` merges the sources and the hub
subscribes to the merge, `s73`).
### direct ### direct
MARTe2 FileReader -> UDPStreamer -> UDPStreamerClient -> FileWriter round-trip. Validates that MARTe2 FileReader -> UDPStreamer -> UDPStreamerClient -> FileWriter round-trip. Validates that
+1 -57
View File
@@ -37,7 +37,6 @@ Offset Size Type Field
| 2 | Client → Server | ACK | Acknowledge a data counter (reserved) | | 2 | Client → Server | ACK | Acknowledge a data counter (reserved) |
| 3 | Client → Server | CONNECT | Request a session | | 3 | Client → Server | CONNECT | Request a session |
| 4 | Client → Server | DISCONNECT | End the session | | 4 | Client → Server | DISCONNECT | End the session |
| 5 | Client → Server | SUBSCRIBE | TCP only: select a stream of a `TCPStreamService` (payload = stream name, 1..255 bytes, or a comma separated list of up to 64 names to multiplex; each server frame then carries its stream's list index in `fragmentIdx`) |
--- ---
@@ -85,28 +84,8 @@ Offset Size Type Field
0xFFFFFFFF = PacketTime (no reference) 0xFFFFFFFF = PacketTime (no reference)
104 32 char[32] unit null-terminated physical unit string 104 32 char[32] unit null-terminated physical unit string
── (total per signal: 136 bytes) ──────────────────────────── ── (total per signal: 136 bytes) ────────────────────────────
── trailer, immediately after the last descriptor ───────────
0 1 uint8 publishMode 0 = Strict, 1 = Accumulate, 2 = Decimate
1 8 uint64 hrtFrequency producer's HRT ticks per second;
0 = unknown
``` ```
### CONFIG trailer
Everything after the descriptors is a trailer that grew field by field, so a
receiver must accept a payload that stops early and must ignore bytes it does
not recognise. An absent `publishMode` means Strict; an absent or zero
`hrtFrequency` means the producer did not publish its tick rate.
`hrtFrequency` is what makes DATA timestamps interpretable off-box. DATA
carries the raw value of the producer's high-resolution counter, and on x86
that counter runs at the TSC frequency — a different number on every model. A
receiver that divides by its own timer's frequency instead is right only while
producer and consumer sit on the same host; anywhere else every batch is laid
out over the wrong span of time. Fall back to the local frequency only when the
field is missing, and reject implausible values (nothing below 1 kHz is a
high-resolution timer).
### Type Codes ### Type Codes
| Code | C type | Bytes/element | | Code | C type | Bytes/element |
@@ -150,9 +129,7 @@ After reassembly, the DATA payload layout is:
``` ```
Offset Size Type Field Offset Size Type Field
────── ──── ────── ──────────────────────────────────────────────────── ────── ──── ────── ────────────────────────────────────────────────────
0 8 uint64 hrtTimestamp producer's high-resolution counter at 0 8 uint64 hrtTimestamp hardware reference timer count at Synchronise()
Synchronise(); divide by the CONFIG
hrtFrequency to get seconds
── for each signal (in config order) ──────────────────────────────────── ── for each signal (in config order) ────────────────────────────────────
varies N×sz — signal data N = numRows×numCols, sz = element size varies N×sz — signal data N = numRows×numCols, sz = element size
(wire size if quantized, raw size otherwise) (wire size if quantized, raw size otherwise)
@@ -194,39 +171,6 @@ the client to reassemble them in any order.
--- ---
## Ordering DATA (required of every receiver)
DATA carries its own `counter` sequence, incremented once per sent packet
(CONFIG is numbered independently). Reassembly completes in arrival order, not
counter order, so a packet reordered or duplicated on the wire surfaces after a
newer one has already been consumed. Its values are well-formed but carry an
older time base: accepting it writes them over samples the consumer already
holds and leaves the span they should have filled empty — a collision on one
side and a hole on the other.
A receiver must therefore drop any DATA packet that does not advance the
counter, and must order it by the *signed* difference:
```c
int32_t delta = (int32_t)(counter - lastCounter); /* survives the uint32 wrap */
if (delta <= 0) { /* stale or duplicate: drop */ }
lost = (uint32_t)delta - 1u; /* packets missing before this one */
```
Comparing the values directly would call the first packet after the wrap stale
and reject the stream from then on.
`lost` matters beyond diagnostics. A consumer that spaces batched samples from
the elapsed time since the previous packet must divide that gap by `lost + 1`
batches; dividing by one batch reports a period too long by exactly that factor
and walks the samples past their own end into the next packet's range. Reset
the sequence on (re)connect: the producer's counter restarts independently.
Implemented in `UDPSClient::AcceptDataCounter` (C++),
`udpsprotocol.SequenceGate` (Go) and `decode_data` (C).
---
## Minimal Python Client Example ## Minimal Python Client Example
```python ```python
-4
View File
@@ -33,9 +33,6 @@ Reply (unicast): `{"type":"pong"}`.
- `addr` — `host:port` of the UDPStreamer control port. - `addr` — `host:port` of the UDPStreamer control port.
- `multicastGroup` / `dataPort` — optional; if present the hub joins the - `multicastGroup` / `dataPort` — optional; if present the hub joins the
multicast group for DATA and keeps a TCP control connection for CONNECT/CONFIG. multicast group for DATA and keeps a TCP control connection for CONNECT/CONFIG.
- `transport` (`"udp"` default, or `"tcp"`) and `streamName` — for a TCP source, the
`TCPStreamService` stream to subscribe to. A comma list (`"a,b"`) multiplexes several
streams over one connection and adds one source per stream, labelled `<label>_<stream>`.
- The hub assigns ids `s1`, `s2`, … and broadcasts an updated `sources` event. - The hub assigns ids `s1`, `s2`, … and broadcasts an updated `sources` event.
### `removeSource` ### `removeSource`
@@ -424,7 +421,6 @@ warning.
[ [
{"label": "wave", "addr": "127.0.0.1:44500"}, {"label": "wave", "addr": "127.0.0.1:44500"},
{"label": "mc", "addr": "127.0.0.1:44501", "multicastGroup": "239.0.0.1", "dataPort": 44502}, {"label": "mc", "addr": "127.0.0.1:44501", "multicastGroup": "239.0.0.1", "dataPort": 44502},
{"label": "svc", "addr": "127.0.0.1:44500", "transport": "tcp", "streamName": "fast,slow"},
{"source": "wave", "signal": "Adc", "scale": 0.00030518, "offset": -1.25, "unit": "V"} {"source": "wave", "signal": "Adc", "scale": 0.00030518, "offset": -1.25, "unit": "V"}
] ]
``` ```
+2 -21
View File
@@ -107,31 +107,12 @@ Hub-side, web-client semantics (`setTrigger` fields in
``` ```
IDLE --arm--> ARMED --edge crossing--> COLLECTING --every source past trigTime+postSec+0.15s--> TRIGGERED IDLE --arm--> ARMED --edge crossing--> COLLECTING --every source past trigTime+postSec+0.15s--> TRIGGERED
TRIGGERED --rearm (single) / auto after holdoffSec (normal, unless stopped)--> ARMED TRIGGERED --rearm (single) / auto ~200ms (normal, unless stopped)--> ARMED
└─ or straight to COLLECTING on a held edge
any --disarm--> IDLE any --disarm--> IDLE
``` ```
`UDPSourceSession` calls `TriggerEngine::CheckSample` for every decoded sample `UDPSourceSession` calls `TriggerEngine::CheckSample` for every decoded sample
of the configured signal (signal index cached per config epoch). of the configured signal (signal index cached per config epoch). Each source is
The comparator keeps running through COLLECTING and TRIGGERED. It cannot fire
there — the capture in flight owns that stretch — but it remembers the first
edge at or past `trigTime + max(postSec, holdoffSec)`, and `Rearm()` fires on
that remembered edge instead of waiting for a fresh one. Without this the engine
is deaf from its own trigger point until the capture has been harvested and the
holdoff has run, which on a sparse pulse train rounds the capture spacing up to
a whole pulse period: at a 1 s window a 1 Hz train was caught at 0.5 Hz, and a
wider window lost whole multiples. The capture is built from the edge's own
timestamp out of rings that still hold everything around it, so honouring it
costs nothing.
`Arm()` and `Rearm()` differ only in this: `Arm()` is the operator's own arm and
discards the held edge (they asked for the next event), while `Rearm()` is the
automatic end-of-capture arm and consumes it. `Rearm()` also keeps the tracked
level, so the first sample after it is compared against its real predecessor
rather than being spent seeding one. `SetConfig()` and `Disarm()` drop the held
edge as well — it was never judged against the new window. Each source is
read `[trigTime−preSec, trigTime+postSec]`, LTTB-capped to 20 000 pts/signal and read `[trigTime−preSec, trigTime+postSec]`, LTTB-capped to 20 000 pts/signal and
appended to a binary **version 2** capture frame; every FSM transition appended to a binary **version 2** capture frame; every FSM transition
broadcasts a `triggerState` event. broadcasts a `triggerState` event.
-4
View File
@@ -58,10 +58,6 @@ Useful flags: `-p <port>` WS port, `-n <pts>` ring size, `-s` skip rebuild,
./Client/streamhub/build/StreamHubClient -host <hub-host> -port 8090 ./Client/streamhub/build/StreamHubClient -host <hub-host> -port 8090
``` ```
A source can use TCP instead of UDP: add `Transport = "tcp"` (and `StreamName = "<stream>"`
for a `TCPStreamService`). `StreamName = "a,b"` takes several streams over one connection,
shown as separate sources `<Label>_a`, `<Label>_b`. See `Docs/TCPStreamer.md`.
Sources can also be added at runtime from either client ("Add source": label + Sources can also be added at runtime from either client ("Add source": label +
`host:port`, optional multicast group/data port). "Save sources" persists the `host:port`, optional multicast group/data port). "Save sources" persists the
runtime list to `streamhub_sources.json` so the hub reloads it on restart. runtime list to `streamhub_sources.json` so the hub reloads it on restart.
-204
View File
@@ -1,204 +0,0 @@
# TCPStreamer / TCPStreamerClient
TCP counterparts of `UDPStreamer` / `UDPStreamerClient`. Same signals,
quantisation, time modes, publishing modes and UDPS wire format; only the
transport differs. Use them when you need lossless, ordered delivery to a few
consumers and do not need multicast.
## How it works
- `TCPStreamer` derives from `UDPStreamer`; `TCPStreamerClient` from
`UDPStreamerClient`. They install a `TCPSServer` / `TCPSClient` through the
`UDPSServerI` / `UDPSClientI` interfaces (`UDPSTransportI.h`), so the RT path
(`Synchronise()` = spinlock + memcpy) and serialisation are shared.
- Frames are UDPS frames (17-byte header + payload) on the byte stream, with
`totalFragments` always 1. The server sends CONFIG on accept; there is no
CONNECT / ACK / keepalive.
- Each client has its own bounded queue (`ClientQueueDepth` DATA frames plus
CONFIG slots). If a client cannot keep up, only its own oldest DATA frames
are dropped (counted by `GetClientDrops()`, logged at most once per second).
A client with no progress for `ClientStallTimeout` seconds is disconnected.
The RT thread and other clients are never affected.
- A new CONFIG (reconfiguration) discards the client's stale queued DATA.
- The client reconnects automatically after any disconnect or protocol error
(bad magic, oversize length, fragmented frame).
## TCPStreamer configuration
All `UDPStreamer` keys apply (see `UDPStreamer.md`) except the UDP-only
`MulticastGroup`, `Interface`, `DataPort`, `ClientTimeout`, which are rejected.
| Key | Default | Meaning |
|---|---|---|
| `Port` | 44500 | TCP listen port (0 = ephemeral, see `GetBoundPort()`) |
| `MaxPayloadSize` | 60000 | Frame budget; sizes Accumulate batches. Frames up to 1 MiB are sent |
| `MaxClients` | 16 | Simultaneous clients (1..1024); extra connections are closed |
| `ClientQueueDepth` | 8 | DATA frames queued per client (1..4096) |
| `ClientStallTimeout` | 5 | Seconds without progress before disconnect; 0 disables |
| `SendBufferSize` | 0 | `SO_SNDBUF` for client sockets; 0 keeps the OS default |
Accessors: `GetBoundPort()`, `GetClientCount()`, `GetClientDrops()`,
`GetStallEvictions()`, plus the inherited `GetDroppedPublications()`.
```
+Streamer = {
Class = TCPStreamer
Port = 44500
ClientQueueDepth = 16
Signals = {
Time = { Type = uint64 }
Value = { Type = float32 }
}
}
```
## TCPStreamerClient configuration
| Key | Default | Meaning |
|---|---|---|
| `ServerAddress` | 127.0.0.1 | Server IPv4 address or host name |
| `Port` | 44500 | Server TCP port |
| `SilenceTimeout` | 0 | Seconds without any byte before reconnecting; 0 disables |
| `ReconnectDelay` | 2 | Seconds between connection attempts |
| `CPUMask`, `StackSize` | default | Receiver thread |
`MulticastGroup`, `DataPort`, `Interface`, `KeepAliveInterval` are rejected.
Accessors: `IsConnected()`, `GetConnectionCount()`, `GetProtocolErrors()`.
The client delivers frames as received; it does no gap or stale-packet
handling.
## Service mode: one in-process server, many streams
`TCPStreamService` is a root-level object that owns the listening socket and a
pool of worker threads. Any number of `TCPStreamer` DataSources (each on its own
real-time thread) publish into it; clients choose a stream by name.
```
+Svc = { Class = TCPStreamService Port = 44500 Workers = 2 }
...
+Fast = { Class = TCPStreamer Service = "Svc" StreamName = "fast" Signals = { ... } }
+Slow = { Class = TCPStreamer Service = "Svc" StreamName = "slow" Signals = { ... } }
+Rx = { Class = TCPStreamerClient Port = 44500 StreamName = "fast" Signals = { ... } }
```
- Wire protocol: plain UDPS over TCP plus one client-to-server frame,
`SUBSCRIBE` (type 5, payload = stream name, 1..255 bytes, or a comma separated
list, see "Joining streams"). The service answers with that stream's CONFIG
and then its DATA frames. A connection that does not subscribe within `HandshakeTimeout`, or
names an unknown stream, is closed (the client reconnects).
- Threads: one acceptor plus `Workers` I/O threads; each connection lives on one
worker. Producers only copy frames into per-connection bounded queues
(drop-oldest) and wake the worker through a self-pipe, so a slow client never
delays a producer or another client. Lock order: topology, stream, connection.
- A stream registers at `Start()` (looked up with
`ObjectRegistryDatabase::Find(Service)`) and unregisters on stop.
- `StreamName` defaults to the DataSource name (max 255 chars). In service mode
`Port`, `MaxClients`, `ClientQueueDepth`, `ClientStallTimeout` and
`SendBufferSize` belong to the service and are rejected on the streamer.
- `TCPStreamerClient` / `TCPSClient` send SUBSCRIBE after every (re)connect when
`StreamName` is set. A plain `TCPSServer` ignores it, so the same client works
against both.
`TCPStreamService` keys:
| Key | Default | Meaning |
|---|---|---|
| `Port` | 44500 | TCP listen port |
| `Workers` | 2 | I/O threads (1..32) |
| `MaxClients` | 64 | Total simultaneous clients (1..1024) |
| `MaxStreams` | 32 | Streams that can register (1..256) |
| `ClientQueueDepth` | 8 | DATA frames queued per client (1..4096) |
| `ClientStallTimeout` | 5 | Seconds without progress before eviction; 0 disables |
| `HandshakeTimeout` | 5 | Seconds a new connection has to SUBSCRIBE (>= 1) |
| `SendBufferSize` | 0 | `SO_SNDBUF`; 0 keeps the OS default |
| `CPUMask`, `StackSize` | default | Worker threads |
## Joining streams
Two independent ways to take several service streams through one cheaper path.
### Multiplexed connection
`SUBSCRIBE` may carry a comma separated list (`"fast,slow"`; at most 64 names,
4096 payload bytes, no duplicates, no `,` in a stream name). One socket then
carries every listed stream, each keeping its own CONFIG, rate and timeline:
- The index of a stream is its position in the list; every server frame carries
it in `fragmentIdx` (`totalFragments` stays 1). A single name is index 0, so
existing clients are unchanged. One unknown name closes the connection.
- Queues are accounted per stream: `ClientQueueDepth` DATA frames *per stream*,
drop-oldest within the same stream only, so a fast stream never evicts a slow
stream's frames. Queued frames are written with one `sendmsg` (up to 16).
- `TCPSClient` accepts `StreamName = "a,b,c"`, demultiplexes by `fragmentIdx`
(`SetStreamListener(i, ...)`) and tracks the counter gap per stream.
`GetStream(i)` returns a `UDPSClientI` facade whose `Start()`/`Stop()` are
reference counted on the shared connection. `TCPStreamerClient` is
single-stream and rejects a comma.
- `GetClientCount()` of a service counts (connection, stream) subscriptions.
### Merged stream: `TCPStreamJoin`
```
+Svc = { Class = TCPStreamService Port = 44500 }
+Fast = { Class = TCPStreamer Service = "Svc" StreamName = "fast" ... }
+Slow = { Class = TCPStreamer Service = "Svc" StreamName = "slow" ... }
+Join = { Class = TCPStreamJoin Service = "Svc" StreamName = "all"
Sources = "fast,slow" Master = "fast" }
```
A root-level object, declared after the service, that publishes a new stream
`all` holding the signals of several streams of the same service:
- CONFIG: source descriptors concatenated in `Sources` order, named
`<stream>_<signal>` (at most 63 characters), `timeSignalIdx` rebased, publish
mode Strict, one HRT frequency (sources with different known frequencies are
refused; an unknown one matches any).
- DATA: one frame per `Master` frame, `[master timestamp][source0 body][source1
body]...`, a body being that source's last DATA payload without its 8-byte
timestamp (sample-and-hold). Nothing is emitted until every source has
delivered one frame. A held value is stamped with the master's time unless the
source has a time signal. No thread is added: the frame is built on the master
producer's thread.
- Only Strict and Decimate sources can be merged; a source whose body size
changes without a new CONFIG is ignored; the merged frame must fit
`MaxPayloadSize` (default 60000).
- The merged stream is registered while every source is registered, removed as
soon as one leaves (its subscribers are dropped and reconnect) and rebuilt when
a source re-registers or changes its CONFIG. A source with a join attached
keeps publishing even with no socket subscriber.
- Keys: `Service` (required), `StreamName` (default object name), `Sources`
(required, 1..16), `Master` (default first source), `MaxPayloadSize`.
Design: `docs/superpowers/specs/2026-11-06-tcp-stream-join-design.md`.
## StreamHub
A StreamHub source can consume over TCP: in the config `Sources` block (and in
`addSource` / the `SourcesFile`) set `Transport = "tcp"` (default `"udp"`) and,
for a service, `StreamName = "<stream>"`. `Addr` is then the TCP server
address. The hub uses `TCPSClient`, which reports the UDPS counter gap so the
accumulate-mode timing estimator tolerates dropped frames.
`StreamName = "a,b,c"` takes several streams over one multiplexed connection and
creates one session per stream, labelled `<Label>_<stream>` (just the stream
name without a label). Session ids are `<node>_<stream>` for a config `Sources`
node and `s<N>` for `addSource`.
## Tests
- GTest: `Test/GTest/TCPSServerGTest.cpp`, `TCPSClientGTest.cpp`,
`TCPStreamerGTest.cpp` (helpers in `TCPTestUtil.h`) and
`TCPStreamServiceGTest.cpp` (queue, protocol, multiplexing, joins,
multi-stream/worker, stalls).
- E2E (`Test/E2E/suite/scenarios.py`): `s58_direct_tcp`, `s59_direct_tcp_service`
(FileReader to FileWriter round trip) and chain scenarios `s60`-`s65` (plain
TCP: scalar, full array, accumulate, decimate/quantise, multi-source,
high rate), `s66`-`s71` (service: same features plus 3- and 6-stream
fan-in), `s72` (hub on one multiplexed connection) and `s73` (merged stream). Scenario configs are generated by `gen_cfg.py`
(`network = "tcp"` / `"tcp_service"`).
## Design
`docs/superpowers/specs/2026-11-05-tcp-streamer-design.md` (section 7 covers
the single-service design implemented by `TCPStreamService`).
-9
View File
@@ -175,7 +175,6 @@ replayed traffic can be decoded without a client.
```c ```c
typedef struct { typedef struct {
uint32_t counter; /* gaps in this sequence are lost datagrams */ uint32_t counter; /* gaps in this sequence are lost datagrams */
uint32_t lost; /* DATA packets missing immediately before this one */
uint64_t hrt; /* producer's high-resolution timer at send */ uint64_t hrt; /* producer's high-resolution timer at send */
double recv_time; /* CLOCK_REALTIME seconds at arrival */ double recv_time; /* CLOCK_REALTIME seconds at arrival */
uint8_t publish_mode; uint8_t publish_mode;
@@ -195,13 +194,6 @@ scalar signal in Accumulate mode, where the producer batches several RT cycles i
and `count == num_samples` — one value per cycle. Arrays are not batched: they appear once and and `count == num_samples` — one value per cycle. Arrays are not batched: they appear once and
apply to the whole packet. `udps_frame_value(f, sig, sample, elem)` applies that rule for you. apply to the whole packet. `udps_frame_value(f, sig, sample, elem)` applies that rule for you.
**Ordering.** Frames reach `on_data` in counter order: a DATA packet that does not advance the
counter — reordered or duplicated on the wire — is dropped rather than delivered, because its
values carry a time base older than data you already have, and placing them would overwrite live
samples while leaving their own span empty. `lost` reports how many packets went missing just
before the frame. If you space samples yourself from the elapsed time since the previous frame,
divide by `lost + 1` batches, not one: the gap covers the missing packets' cycles too.
**Timestamps.** The protocol does not put a timestamp on every element; how to date them depends **Timestamps.** The protocol does not put a timestamp on every element; how to date them depends
on the signal's `time_mode` (see [Protocol.md](Protocol.md#time-mode-codes)): on the signal's `time_mode` (see [Protocol.md](Protocol.md#time-mode-codes)):
@@ -231,7 +223,6 @@ udps_client_stats(cli, &s);
| `frames_delivered` | DATA packets decoded and passed to `on_data`. | | `frames_delivered` | DATA packets decoded and passed to `on_data`. |
| `config_updates` | CONFIG packets applied. | | `config_updates` | CONFIG packets applied. |
| `counter_gaps` | Missing packet counters — datagrams lost on the wire or in the kernel. | | `counter_gaps` | Missing packet counters — datagrams lost on the wire or in the kernel. |
| `stale_packets` | DATA packets dropped for not advancing the counter: reordered or duplicated on the wire. |
| `fragments_dropped` | Duplicate, stale or unplaceable fragments; a non-zero value with `counter_gaps` means fragmented updates are arriving incomplete. | | `fragments_dropped` | Duplicate, stale or unplaceable fragments; a non-zero value with `counter_gaps` means fragmented updates are arriving incomplete. |
| `reconnects` | Sessions re-established after a silence timeout. | | `reconnects` | Sessions re-established after a silence timeout. |
-6
View File
@@ -18,9 +18,6 @@ core:
$(MAKE) -C Source/Components/Interfaces/UDPStream -f Makefile.gcc $(MAKE) -C Source/Components/Interfaces/UDPStream -f Makefile.gcc
$(MAKE) -C Source/Components/DataSources/UDPStreamer -f Makefile.gcc $(MAKE) -C Source/Components/DataSources/UDPStreamer -f Makefile.gcc
$(MAKE) -C Source/Components/DataSources/UDPStreamerClient -f Makefile.gcc $(MAKE) -C Source/Components/DataSources/UDPStreamerClient -f Makefile.gcc
$(MAKE) -C Source/Components/Interfaces/TCPStreamService -f Makefile.gcc
$(MAKE) -C Source/Components/DataSources/TCPStreamer -f Makefile.gcc
$(MAKE) -C Source/Components/DataSources/TCPStreamerClient -f Makefile.gcc
$(MAKE) -C Source/Components/GAMs/SineArrayGAM -f Makefile.gcc $(MAKE) -C Source/Components/GAMs/SineArrayGAM -f Makefile.gcc
$(MAKE) -C Source/Components/GAMs/TimeArrayGAM -f Makefile.gcc $(MAKE) -C Source/Components/GAMs/TimeArrayGAM -f Makefile.gcc
$(MAKE) -C Source/Components/Interfaces/TCPLogger -f Makefile.gcc $(MAKE) -C Source/Components/Interfaces/TCPLogger -f Makefile.gcc
@@ -38,9 +35,6 @@ clean:
$(MAKE) -C Source/Components/Interfaces/UDPStream -f Makefile.gcc clean $(MAKE) -C Source/Components/Interfaces/UDPStream -f Makefile.gcc clean
$(MAKE) -C Source/Components/DataSources/UDPStreamer -f Makefile.gcc clean $(MAKE) -C Source/Components/DataSources/UDPStreamer -f Makefile.gcc clean
$(MAKE) -C Source/Components/DataSources/UDPStreamerClient -f Makefile.gcc clean $(MAKE) -C Source/Components/DataSources/UDPStreamerClient -f Makefile.gcc clean
$(MAKE) -C Source/Components/Interfaces/TCPStreamService -f Makefile.gcc clean
$(MAKE) -C Source/Components/DataSources/TCPStreamer -f Makefile.gcc clean
$(MAKE) -C Source/Components/DataSources/TCPStreamerClient -f Makefile.gcc clean
$(MAKE) -C Source/Components/GAMs/SineArrayGAM -f Makefile.gcc clean $(MAKE) -C Source/Components/GAMs/SineArrayGAM -f Makefile.gcc clean
$(MAKE) -C Source/Components/GAMs/TimeArrayGAM -f Makefile.gcc clean $(MAKE) -C Source/Components/GAMs/TimeArrayGAM -f Makefile.gcc clean
$(MAKE) -C Source/Components/Interfaces/TCPLogger -f Makefile.gcc clean $(MAKE) -C Source/Components/Interfaces/TCPLogger -f Makefile.gcc clean
+2 -22
View File
@@ -12,7 +12,6 @@ This repository integrates two complementary capabilities:
| Capability | Component | Purpose | | Capability | Component | Purpose |
| --------------------- | ------------------------ | ------------------------------------------------------------------------------------------------------------- | | --------------------- | ------------------------ | ------------------------------------------------------------------------------------------------------------- |
| **Signal streaming** | `UDPStreamer` DataSource | Continuously stream selected signals to a browser-based oscilloscope over UDP | | **Signal streaming** | `UDPStreamer` DataSource | Continuously stream selected signals to a browser-based oscilloscope over UDP |
| **Signal streaming (TCP)** | `TCPStreamer` / `TCPStreamerClient` DataSources, `TCPStreamService`, `TCPStreamJoin` | Same UDPS stream over reliable TCP; one in-process server for many streams; multiplexed connections and merged streams |
| **Signal debugging** | `DebugService` Interface | On-demand signal tracing, value forcing, and conditional breakpoints — zero application code changes required | | **Signal debugging** | `DebugService` Interface | On-demand signal tracing, value forcing, and conditional breakpoints — zero application code changes required |
| **Sine generation** | `SineArrayGAM` | Generate continuous sine-wave arrays for testing and simulation | | **Sine generation** | `SineArrayGAM` | Generate continuous sine-wave arrays for testing and simulation |
| **Time stamping** | `TimeArrayGAM` | Provide time-reference arrays aligned to an RT cycle | | **Time stamping** | `TimeArrayGAM` | Provide time-reference arrays aligned to an RT cycle |
@@ -31,11 +30,8 @@ MARTe_Integrated_components/
│ └── Client/go/ Go packages: udpsprotocol, wshub │ └── Client/go/ Go packages: udpsprotocol, wshub
├── Source/Components/ ├── Source/Components/
│ ├── DataSources/UDPStreamer/ Real-time UDP signal streaming DataSource │ ├── DataSources/UDPStreamer/ Real-time UDP signal streaming DataSource
│ ├── DataSources/TCPStreamer/ TCP variant of UDPStreamer (plus TCPStreamerClient)
│ ├── GAMs/SineArrayGAM/ Sine-wave array generator GAM │ ├── GAMs/SineArrayGAM/ Sine-wave array generator GAM
│ ├── GAMs/TimeArrayGAM/ Time-reference array GAM │ ├── GAMs/TimeArrayGAM/ Time-reference array GAM
│ ├── Interfaces/UDPStream/ UDPS server/client transports (UDP and TCP)
│ ├── Interfaces/TCPStreamService/ In-process multi-stream TCP server + TCPStreamJoin
│ ├── Interfaces/DebugService/ Signal tracing/forcing/breakpoint Interface │ ├── Interfaces/DebugService/ Signal tracing/forcing/breakpoint Interface
│ └── Interfaces/TCPLogger/ TCP log-forwarding LoggerConsumerI │ └── Interfaces/TCPLogger/ TCP log-forwarding LoggerConsumerI
├── Test/ ├── Test/
@@ -63,21 +59,6 @@ sending a `CONNECT` packet; the server then sends `CONFIG` (signal metadata) and
See `Docs/UDPStreamer.md` and `Docs/Protocol.md`. See `Docs/UDPStreamer.md` and `Docs/Protocol.md`.
### TCPStreamer / TCPStreamService / TCPStreamJoin
`TCPStreamer` and `TCPStreamerClient` are the TCP counterparts of the UDP DataSources: same
UDPS framing, per-client bounded queues (a slow client only drops its own frames).
`TCPStreamService` is an optional single in-process server (acceptor + worker threads)
shared by many `TCPStreamer`s; clients pick a stream by name with a `SUBSCRIBE` frame.
- **Multiplexing**: a `SUBSCRIBE` may list several streams (`"fast,slow"`) to carry them over
one socket, each with its own CONFIG and rate. StreamHub does this for a comma separated
`StreamName`.
- **Joining**: `TCPStreamJoin` publishes a new stream merging several streams of the service
(`<stream>_<signal>`, sample-and-hold, paced by a master source).
See `Docs/TCPStreamer.md`.
### SineArrayGAM ### SineArrayGAM
Generates a continuous float32 sine-wave array every RT cycle. Used as a signal Generates a continuous float32 sine-wave array every RT cycle. Used as a signal
@@ -138,7 +119,7 @@ clients on a configurable port. Works as a sidecar to `DebugService`.
### StreamHub Application ### StreamHub Application
Headless C++ hub (`Source/Applications/StreamHub/`) that aggregates multiple Headless C++ hub (`Source/Applications/StreamHub/`) that aggregates multiple
UDPStreamer / TCPStreamer sources (`Transport = "tcp"`, optional `StreamName`) and serves them to oscilloscope clients over WebSocket UDPStreamer sources and serves them to oscilloscope clients over WebSocket
(port 8090): ring buffers with wall-clock time calibration, LTTB decimation, (port 8090): ring buffers with wall-clock time calibration, LTTB decimation,
hub-side trigger engine, per-window zoom. Clients: browser SPA hub-side trigger engine, per-window zoom. Clients: browser SPA
(`Client/webui` + `Client/udpstreamer/static`) and native ImGui desktop client (`Client/webui` + `Client/udpstreamer/static`) and native ImGui desktop client
@@ -151,7 +132,7 @@ See `Docs/StreamHub-UserGuide.md`, `Docs/StreamHub-API.md` and
### UDPS Protocol ### UDPS Protocol
The `Common/UDP/UDPSProtocol.h` header defines the shared binary wire format used by The `Common/UDP/UDPSProtocol.h` header defines the shared binary wire format used by
`UDPStreamer`, `TCPStreamer` and `DebugService`. It is intentionally free of MARTe2-specific both `UDPStreamer` and `DebugService`. It is intentionally free of MARTe2-specific
dependencies so it can also be used by Go clients (via `Common/Client/go/udpsprotocol`). dependencies so it can also be used by Go clients (via `Common/Client/go/udpsprotocol`).
See `Docs/Protocol.md`. See `Docs/Protocol.md`.
@@ -263,7 +244,6 @@ Open `http://localhost:9090`, explore the object tree, trace signals, force valu
| ----------------------------- | -------------------------------------------------------------- | | ----------------------------- | -------------------------------------------------------------- |
| `Docs/Protocol.md` | UDPS binary wire protocol specification | | `Docs/Protocol.md` | UDPS binary wire protocol specification |
| `Docs/UDPStreamer.md` | UDPStreamer DataSource configuration reference | | `Docs/UDPStreamer.md` | UDPStreamer DataSource configuration reference |
| `Docs/TCPStreamer.md` | TCPStreamer, TCPStreamService, multiplexing and TCPStreamJoin |
| `Docs/UDPS-C-Client.md` | Standalone C/C++ UDPS receiver library (`Common/Client/c`) | | `Docs/UDPS-C-Client.md` | Standalone C/C++ UDPS receiver library (`Common/Client/c`) |
| `Docs/SineArrayGAM.md` | SineArrayGAM configuration reference | | `Docs/SineArrayGAM.md` | SineArrayGAM configuration reference |
| `Docs/DebugService.md` | DebugService TCP API and architecture | | `Docs/DebugService.md` | DebugService TCP API and architecture |
+10 -208
View File
@@ -92,7 +92,6 @@ StreamHub::StreamHub()
memset(calibration_, 0, sizeof(CalibrationEntry) * kMaxCalibration); memset(calibration_, 0, sizeof(CalibrationEntry) * kMaxCalibration);
for (uint32 i = 0u; i < kMaxSessions; i++) { for (uint32 i = 0u; i < kMaxSessions; i++) {
sessionActive_[i] = false; sessionActive_[i] = false;
sessionMux_[i] = -1;
configBroadcast_[i] = false; configBroadcast_[i] = false;
capHarvested_[i] = false; capHarvested_[i] = false;
for (uint32 s = 0u; s < UDPSS_MAX_SIGNALS; s++) { for (uint32 s = 0u; s < UDPSS_MAX_SIGNALS; s++) {
@@ -274,33 +273,11 @@ bool StreamHub::Initialise(StructuredDataI &cfg) {
} }
cfg.Read("DataPort", dataPort32); cfg.Read("DataPort", dataPort32);
char transport[8] = "udp";
char streamName[256] = "";
StreamString trStr;
if (cfg.Read("Transport", trStr)) {
strncpy(transport, trStr.Buffer(), sizeof(transport) - 1u);
}
StreamString snStr;
if (cfg.Read("StreamName", snStr)) {
strncpy(streamName, snStr.Buffer(), sizeof(streamName) - 1u);
}
if ((strcmp(transport, "tcp") == 0) && (strchr(streamName, ',') != static_cast<char *>(0))) {
/* Several streams over one connection: slots are allocated by the helper. */
if (!AddMuxSourceInternal(label, addr, port32, addr, streamName, nodeId.Buffer())) {
REPORT_ERROR_STATIC(MARTe::ErrorManagement::Warning,
"StreamHub: failed to start multiplexed source '%s'.", nodeId.Buffer());
}
cfg.MoveToAncestor(1u);
continue;
}
sessions_[numSessions_].SetRingCapacities(ringTemporal_, ringScalar_); sessions_[numSessions_].SetRingCapacities(ringTemporal_, ringScalar_);
bool ok = sessions_[numSessions_].Initialise( bool ok = sessions_[numSessions_].Initialise(
nodeId.Buffer(), label, addr, nodeId.Buffer(), label, addr,
static_cast<uint16>(port32), maxPoints_, static_cast<uint16>(port32), maxPoints_,
mcGroup, static_cast<uint16>(dataPort32), mcGroup, static_cast<uint16>(dataPort32));
transport, streamName);
if (ok) { if (ok) {
sessions_[numSessions_].SetRecorderConfig(recorderCfg_); sessions_[numSessions_].SetRecorderConfig(recorderCfg_);
@@ -1224,20 +1201,15 @@ void StreamHub::HandleAddSource(const char *json) {
JsonGetString(json, "addr", addr, sizeof(addr)); JsonGetString(json, "addr", addr, sizeof(addr));
JsonGetString(json, "multicastGroup", mcGroup, sizeof(mcGroup)); JsonGetString(json, "multicastGroup", mcGroup, sizeof(mcGroup));
JsonGetFloat(json, "dataPort", dataPortF); JsonGetFloat(json, "dataPort", dataPortF);
char transport[8] = "udp";
char streamName[256] = "";
JsonGetString(json, "transport", transport, sizeof(transport));
JsonGetString(json, "streamName", streamName, sizeof(streamName));
if (AddSourceInternal(label, addr, mcGroup, if (AddSourceInternal(label, addr, mcGroup,
static_cast<uint16>(dataPortF), transport, streamName)) { static_cast<uint16>(dataPortF))) {
BroadcastSources(); BroadcastSources();
} }
} }
bool StreamHub::AddSourceInternal(const char *label, const char *addrPort, bool StreamHub::AddSourceInternal(const char *label, const char *addrPort,
const char *mcGroup, uint16 dataPort, const char *mcGroup, uint16 dataPort) {
const char *transport, const char *streamName) {
if ((addrPort == static_cast<const char *>(0)) || (addrPort[0] == '\0')) { if ((addrPort == static_cast<const char *>(0)) || (addrPort[0] == '\0')) {
return false; return false;
} }
@@ -1254,12 +1226,6 @@ bool StreamHub::AddSourceInternal(const char *label, const char *addrPort,
*colon = '\0'; *colon = '\0';
} }
if ((transport != static_cast<const char *>(0)) && (strcmp(transport, "tcp") == 0) &&
(streamName != static_cast<const char *>(0)) &&
(strchr(streamName, ',') != static_cast<const char *>(0))) {
return AddMuxSourceInternal(label, host, port, addrPort, streamName);
}
/* Whole add is serialized against other WS-thread add/remove requests. /* Whole add is serialized against other WS-thread add/remove requests.
* The push thread never takes this mutex: it only observes the * The push thread never takes this mutex: it only observes the
* sessionActive_[] flag, which is set last (after a successful Start). */ * sessionActive_[] flag, which is set last (after a successful Start). */
@@ -1290,7 +1256,7 @@ bool StreamHub::AddSourceInternal(const char *label, const char *addrPort,
sessions_[idx].SetRingCapacities(ringTemporal_, ringScalar_); sessions_[idx].SetRingCapacities(ringTemporal_, ringScalar_);
bool ok = sessions_[idx].Initialise(id, lbl, host, bool ok = sessions_[idx].Initialise(id, lbl, host,
static_cast<uint16>(port), maxPoints_, static_cast<uint16>(port), maxPoints_,
mcGroup, dataPort, transport, streamName); mcGroup, dataPort);
if (ok) { if (ok) {
sessions_[idx].SetRecorderConfig(recorderCfg_); sessions_[idx].SetRecorderConfig(recorderCfg_);
ok = sessions_[idx].Start(); ok = sessions_[idx].Start();
@@ -1309,151 +1275,14 @@ bool StreamHub::AddSourceInternal(const char *label, const char *addrPort,
return ok; return ok;
} }
bool StreamHub::AddMuxSourceInternal(const char *label, const char *host, uint32 port, bool StreamHub::SourceIsActive(const char *addrPort) {
const char *addrPort, const char *streamList,
const char *idPrefix) {
char list[MARTe::UDPS_TCP_MAX_SUBSCRIBE_PAYLOAD + 1u];
strncpy(list, streamList, sizeof(list) - 1u);
list[sizeof(list) - 1u] = '\0';
const char *names[kMaxSessions];
uint32 n = 0u;
char *cur = list;
while (cur != static_cast<char *>(0)) {
char *comma = strchr(cur, ',');
if (comma != static_cast<char *>(0)) { *comma = '\0'; }
if ((cur[0] == '\0') || (n >= kMaxSessions)) {
REPORT_ERROR_STATIC(MARTe::ErrorManagement::Warning,
"StreamHub: bad stream list '%s' (empty name or more than %u streams).",
streamList, kMaxSessions);
return false;
}
names[n++] = cur;
cur = (comma != static_cast<char *>(0)) ? (comma + 1) : static_cast<char *>(0);
}
(void) sessionsMutex_.FastLock();
uint32 slots[kMaxSessions];
uint32 found = 0u;
for (uint32 i = 0u; (i < kMaxSessions) && (found < n); i++) {
if (!sessionActive_[i]) { slots[found++] = i; }
}
uint32 mux = kMaxSessions;
for (uint32 i = 0u; i < kMaxSessions; i++) {
if (muxClients_[i].client == static_cast<MARTe::TCPSClient *>(0)) { mux = i; break; }
}
if ((found < n) || (mux == kMaxSessions)) {
sessionsMutex_.FastUnLock();
REPORT_ERROR_STATIC(MARTe::ErrorManagement::Warning,
"StreamHub: not enough free sessions for %u streams, ignoring addSource.", n);
return false;
}
MARTe::TCPSClient *client = new MARTe::TCPSClient();
ConfigurationDatabase cfg;
(void) cfg.Write("ServerAddr", host);
(void) cfg.Write("Port", port);
(void) cfg.Write("StreamName", streamList);
bool ok = client->Initialise(cfg) && (client->GetNumStreams() == n);
if (!ok) {
delete client;
sessionsMutex_.FastUnLock();
REPORT_ERROR_STATIC(MARTe::ErrorManagement::Warning,
"StreamHub: could not set up the multiplexed receiver for '%s'.", streamList);
return false;
}
muxClients_[mux].client = client;
muxClients_[mux].refs = 0u;
const char *base = ((label != static_cast<const char *>(0)) && (label[0] != '\0')) ? label
: static_cast<const char *>(0);
/* Initialise every session (listeners attached) before any can receive. */
uint32 initialised = 0u;
for (uint32 k = 0u; (k < n) && ok; k++) {
const uint32 idx = slots[k];
char id[16 + 256];
if (idPrefix != static_cast<const char *>(0)) {
snprintf(id, sizeof(id), "%s_%s", idPrefix, names[k]);
} else {
snprintf(id, sizeof(id), "s%u", nextSourceId_++);
}
char lbl[160];
if (base != static_cast<const char *>(0)) {
snprintf(lbl, sizeof(lbl), "%s_%s", base, names[k]);
} else {
snprintf(lbl, sizeof(lbl), "%s", names[k]);
}
configBroadcast_[idx] = false;
for (uint32 s = 0u; s < UDPSS_MAX_SIGNALS; s++) { pushCursor_[idx][s] = 0u; }
sessions_[idx].SetRingCapacities(ringTemporal_, ringScalar_);
ok = sessions_[idx].Initialise(id, lbl, host, static_cast<uint16>(port), maxPoints_,
"", 0u, "tcp", names[k], client->GetStream(k));
if (ok) {
sessionMux_[idx] = static_cast<MARTe::int32>(mux);
muxClients_[mux].refs++;
initialised++;
}
}
for (uint32 k = 0u; (k < initialised) && ok; k++) {
sessions_[slots[k]].SetRecorderConfig(recorderCfg_);
ok = sessions_[slots[k]].Start();
}
if (ok) {
for (uint32 k = 0u; k < n; k++) {
sessionActive_[slots[k]] = true;
numSessions_++;
}
} else {
for (uint32 k = 0u; k < initialised; k++) { ReleaseMuxSlot(slots[k]); }
if (muxClients_[mux].client != static_cast<MARTe::TCPSClient *>(0)) {
delete muxClients_[mux].client; /* no session ever attached */
muxClients_[mux].client = static_cast<MARTe::TCPSClient *>(0);
}
}
sessionsMutex_.FastUnLock();
if (ok) {
REPORT_ERROR_STATIC(MARTe::ErrorManagement::Information,
"StreamHub: added %u multiplexed sessions over one connection (%s).", n, addrPort);
}
return ok;
}
void StreamHub::ReleaseMuxSlot(uint32 slot) {
/* Caller holds sessionsMutex_. */
if (sessionMux_[slot] < 0) { return; }
const uint32 m = static_cast<uint32>(sessionMux_[slot]);
sessions_[slot].DetachClient();
sessionMux_[slot] = -1;
if ((muxClients_[m].refs > 0u) && (--muxClients_[m].refs == 0u)) {
delete muxClients_[m].client;
muxClients_[m].client = static_cast<MARTe::TCPSClient *>(0);
}
}
bool StreamHub::SourceIsActive(const char *addrPort, const char *streamName) {
if (strchr(streamName, ',') != static_cast<const char *>(0)) {
/* A list is active when every stream of it is. */
char list[MARTe::UDPS_TCP_MAX_SUBSCRIBE_PAYLOAD + 1u];
strncpy(list, streamName, sizeof(list) - 1u);
list[sizeof(list) - 1u] = '\0';
bool all = true;
char *cur = list;
while (all && (cur != static_cast<char *>(0))) {
char *comma = strchr(cur, ',');
if (comma != static_cast<char *>(0)) { *comma = '\0'; }
all = SourceIsActive(addrPort, cur);
cur = (comma != static_cast<char *>(0)) ? (comma + 1) : static_cast<char *>(0);
}
return all;
}
for (uint32 i = 0u; i < kMaxSessions; i++) { for (uint32 i = 0u; i < kMaxSessions; i++) {
if (!sessionActive_[i]) { continue; } if (!sessionActive_[i]) { continue; }
StreamString adr = sessions_[i].GetAddr(); StreamString adr = sessions_[i].GetAddr();
char cur[96]; char cur[96];
(void) snprintf(cur, sizeof(cur), "%s:%u", adr.Buffer(), (void) snprintf(cur, sizeof(cur), "%s:%u", adr.Buffer(),
static_cast<uint32>(sessions_[i].GetPort())); static_cast<uint32>(sessions_[i].GetPort()));
StreamString sn = sessions_[i].GetStreamName(); if (strcmp(cur, addrPort) == 0) { return true; }
if ((strcmp(cur, addrPort) == 0) && (strcmp(sn.Buffer(), streamName) == 0)) { return true; }
} }
return false; return false;
} }
@@ -1506,16 +1335,11 @@ bool StreamHub::LoadSourcesFile(bool skipActive, bool clearCalibration) {
(void) JsonGetString(obj, "label", label, sizeof(label)); (void) JsonGetString(obj, "label", label, sizeof(label));
(void) JsonGetString(obj, "multicastGroup", mcGroup, sizeof(mcGroup)); (void) JsonGetString(obj, "multicastGroup", mcGroup, sizeof(mcGroup));
(void) JsonGetFloat(obj, "dataPort", dataPortF); (void) JsonGetFloat(obj, "dataPort", dataPortF);
char transport[8] = "udp"; if (skipActive && SourceIsActive(addr)) {
char streamName[256] = "";
(void) JsonGetString(obj, "transport", transport, sizeof(transport));
(void) JsonGetString(obj, "streamName", streamName, sizeof(streamName));
if (skipActive && SourceIsActive(addr, streamName)) {
/* Already streaming — leave the live session untouched. */ /* Already streaming — leave the live session untouched. */
} }
else if (AddSourceInternal(label, addr, mcGroup, else if (AddSourceInternal(label, addr, mcGroup,
static_cast<uint16>(dataPortF), static_cast<uint16>(dataPortF))) {
transport, streamName)) {
nLoaded++; nLoaded++;
} }
} }
@@ -1590,14 +1414,6 @@ void StreamHub::HandleSaveSources() {
static_cast<uint32>(dpt)); static_cast<uint32>(dpt));
} }
} }
StreamString trn = sessions_[i].GetTransport();
if (strcmp(trn.Buffer(), "tcp") == 0) {
(void) fprintf(f, ",\n \"transport\": \"tcp\"");
StreamString sn = sessions_[i].GetStreamName();
if (sn.Size() > 0u) {
(void) fprintf(f, ",\n \"streamName\": \"%s\"", sn.Buffer());
}
}
(void) fprintf(f, "\n }"); (void) fprintf(f, "\n }");
nSaved++; nSaved++;
} }
@@ -1684,26 +1500,16 @@ void StreamHub::HandleRemoveSource(const char *json) {
break; break;
} }
} }
bool shared = false;
if (found < kMaxSessions) { if (found < kMaxSessions) {
sessionActive_[found] = false; sessionActive_[found] = false;
configBroadcast_[found] = false; /* allow re-broadcast on slot reuse */ configBroadcast_[found] = false; /* allow re-broadcast on slot reuse */
numSessions_--; numSessions_--;
if (sessionMux_[found] >= 0) {
/* One stream of a shared receiver: stop it and drop its listener
* under the lock, so the slot cannot be reused underneath. */
sessions_[found].Stop();
ReleaseMuxSlot(found);
shared = true;
}
} }
sessionsMutex_.FastUnLock(); sessionsMutex_.FastUnLock();
if (found == kMaxSessions) { return; } if (found == kMaxSessions) { return; }
if (!shared) { sessions_[found].Stop();
sessions_[found].Stop();
}
REPORT_ERROR_STATIC(MARTe::ErrorManagement::Information, REPORT_ERROR_STATIC(MARTe::ErrorManagement::Information,
"StreamHub: removed session '%s'.", id); "StreamHub: removed session '%s'.", id);
@@ -1930,11 +1736,7 @@ void StreamHub::TriggerTick(float64 wallNowS) {
(wallNowS >= rearmAtWallS_)) { (wallNowS >= rearmAtWallS_)) {
rearmPending_ = false; rearmPending_ = false;
if (!trigger_.GetStopped()) { if (!trigger_.GetStopped()) {
/* Rearm, not Arm: an edge that arrived while this capture was being trigger_.Arm();
* collected is fired on at once instead of being thrown away, which
* is what kept sparse pulse trains from being caught at their own
* rate. */
trigger_.Rearm();
} }
} }
+3 -34
View File
@@ -231,27 +231,11 @@ private:
* @return true if the session was started. * @return true if the session was started.
*/ */
bool AddSourceInternal(const char *label, const char *addrPort, bool AddSourceInternal(const char *label, const char *addrPort,
const char *mcGroup, uint16 dataPort, const char *mcGroup, uint16 dataPort);
const char *transport = "udp",
const char *streamName = "");
/**
* @brief TCP source whose streamName is a comma separated list "a,b,c": one
* TCPSClient (one socket) feeds one session per stream, labelled
* "<label>_<stream>" (just "<stream>" without a label). Session ids are
* "s<N>", or "<idPrefix>_<stream>" when @p idPrefix (the config node id) is given.
* @return true if every session was started (none is left behind otherwise).
*/
bool AddMuxSourceInternal(const char *label, const char *host, uint32 port,
const char *addrPort, const char *streamList,
const char *idPrefix = static_cast<const char *>(0));
/** @brief Detach @p slot from its shared receiver; frees the receiver with its last session. */
void ReleaseMuxSlot(uint32 slot);
/** /**
* @brief Load sources and calibration from sourcesFile_ (a flat JSON array * @brief Load sources and calibration from sourcesFile_ (a flat JSON array
* of {"label","addr","multicastGroup","dataPort","transport","streamName"} source blocks and * of {"label","addr","multicastGroup","dataPort"} source blocks and
* {"source","signal","scale","offset","unit"} calibration blocks). * {"source","signal","scale","offset","unit"} calibration blocks).
* @param skipActive when true, a source whose "host:port" is already * @param skipActive when true, a source whose "host:port" is already
* streaming is left alone instead of being started a second time. * streaming is left alone instead of being started a second time.
@@ -263,7 +247,7 @@ private:
bool LoadSourcesFile(bool skipActive, bool clearCalibration = false); bool LoadSourcesFile(bool skipActive, bool clearCalibration = false);
/** @return true if a session for this "host:port" is already active. */ /** @return true if a session for this "host:port" is already active. */
bool SourceIsActive(const char *addrPort, const char *streamName = ""); bool SourceIsActive(const char *addrPort);
/** /**
* @brief Store or replace one calibration entry. An identity entry * @brief Store or replace one calibration entry. An identity entry
@@ -296,21 +280,6 @@ private:
/* ---- Members --------------------------------------------------------- */ /* ---- Members --------------------------------------------------------- */
/** A multiplexed receiver shared by several sessions (deleted with the last one). */
struct MuxClient {
MuxClient() : client(static_cast<MARTe::TCPSClient *>(0)), refs(0u) {}
~MuxClient() { delete client; }
MARTe::TCPSClient *client;
uint32 refs;
private:
MuxClient(const MuxClient &);
MuxClient &operator=(const MuxClient &);
};
/* Declared before sessions_ so it is destroyed after them: a session stops
* its (facade of a) shared receiver from its own destructor. */
MuxClient muxClients_[kMaxSessions];
MARTe::int32 sessionMux_[kMaxSessions]; ///< Index into muxClients_, or -1
UDPSourceSession sessions_[kMaxSessions]; UDPSourceSession sessions_[kMaxSessions];
uint32 numSessions_; ///< Count of active sessions (not an upper slot bound) uint32 numSessions_; ///< Count of active sessions (not an upper slot bound)
bool sessionActive_[kMaxSessions]; ///< Per-slot in-use flag (slots are not compacted on removal) bool sessionActive_[kMaxSessions]; ///< Per-slot in-use flag (slots are not compacted on removal)
+25 -77
View File
@@ -19,17 +19,7 @@ TriggerEngine::TriggerEngine()
trigTime_(0.0), trigTime_(0.0),
firedPreSec_(0.0), firedPreSec_(0.0),
firedPostSec_(0.0), firedPostSec_(0.0),
firedValid_(false), firedValid_(false) {
pendingTime_(0.0),
pendingValid_(false) {
}
void TriggerEngine::LatchWindowLocked(float64 t) {
state_ = kTrigCollecting;
trigTime_ = t;
firedPreSec_ = config_.windowSec * config_.prePercent / 100.0;
firedPostSec_ = config_.windowSec - firedPreSec_;
firedValid_ = true;
} }
void TriggerEngine::SetConfig(const TriggerConfig &cfg) { void TriggerEngine::SetConfig(const TriggerConfig &cfg) {
@@ -50,9 +40,6 @@ void TriggerEngine::SetConfig(const TriggerConfig &cfg) {
epoch_++; epoch_++;
prevValid_ = false; prevValid_ = false;
prevValue_ = 0.0; prevValue_ = 0.0;
/* An edge held over from the old configuration would be latched against the
* new window, which it was never judged against. */
pendingValid_ = false;
mutex_.FastUnLock(); mutex_.FastUnLock();
} }
@@ -72,40 +59,19 @@ uint32 TriggerEngine::GetConfigEpoch() const {
void TriggerEngine::Arm() { void TriggerEngine::Arm() {
(void) mutex_.FastLock(); (void) mutex_.FastLock();
state_ = kTrigArmed; state_ = kTrigArmed;
prevValid_ = false; prevValid_ = false;
prevValue_ = 0.0; prevValue_ = 0.0;
pendingValid_ = false;
mutex_.FastUnLock();
}
void TriggerEngine::Rearm() {
(void) mutex_.FastLock();
if (pendingValid_) {
const float64 t = pendingTime_;
pendingValid_ = false;
LatchWindowLocked(t);
REPORT_ERROR_STATIC(MARTe::ErrorManagement::Information,
"TriggerEngine: rearmed onto the edge held at t=%.6f "
"(pre=%.4fs post=%.4fs)",
t, firedPreSec_, firedPostSec_);
}
else {
/* prevValue_/prevValid_ are deliberately kept: the comparator ran right
* through the dead time, so the next sample has a real predecessor. */
state_ = kTrigArmed;
}
mutex_.FastUnLock(); mutex_.FastUnLock();
} }
void TriggerEngine::Disarm() { void TriggerEngine::Disarm() {
(void) mutex_.FastLock(); (void) mutex_.FastLock();
state_ = kTrigIdle; state_ = kTrigIdle;
stopped_ = false; stopped_ = false;
prevValid_ = false; prevValid_ = false;
prevValue_ = 0.0; prevValue_ = 0.0;
firedValid_ = false; firedValid_ = false;
pendingValid_ = false;
mutex_.FastUnLock(); mutex_.FastUnLock();
} }
@@ -130,10 +96,7 @@ void TriggerEngine::CheckSample(float64 t, float64 v) {
lastTime_ = t; lastTime_ = t;
lastTimeValid_ = true; lastTimeValid_ = true;
/* A capture in flight does not stop the comparator; it only changes what an if (state_ != kTrigArmed) {
* edge does. See pendingTime_. */
const bool inFlight = (state_ == kTrigCollecting) || (state_ == kTrigTriggered);
if ((state_ != kTrigArmed) && !inFlight) {
mutex_.FastUnLock(); mutex_.FastUnLock();
return; return;
} }
@@ -158,35 +121,16 @@ void TriggerEngine::CheckSample(float64 t, float64 v) {
} }
if (fired) { if (fired) {
if (!inFlight) { state_ = kTrigCollecting;
/* Latch the window at fire time so later config edits do not trigTime_ = t;
* affect this capture (web client snap._preS/_postS). */ /* Latch the window at fire time so later config edits do not
LatchWindowLocked(t); * affect this capture (web client snap._preS/_postS). */
REPORT_ERROR_STATIC(MARTe::ErrorManagement::Information, firedPreSec_ = config_.windowSec * config_.prePercent / 100.0;
"TriggerEngine: fired at t=%.6f (pre=%.4fs post=%.4fs)", firedPostSec_ = config_.windowSec - firedPreSec_;
t, firedPreSec_, firedPostSec_); firedValid_ = true;
} REPORT_ERROR_STATIC(MARTe::ErrorManagement::Information,
else if (!pendingValid_ && firedValid_) { "TriggerEngine: fired at t=%.6f (pre=%.4fs post=%.4fs)",
/* The earliest trigger point a new capture may take. The one in t, firedPreSec_, firedPostSec_);
* flight owns everything up to the end of its own post-window, and
* the holdoff — a guard against re-triggering on the ringing of the
* SAME event — is measured from its trigger point too, so the two
* overlap rather than add.
*
* Keep only the FIRST qualifying edge: a later one would deliver
* the same capture a pulse further on and skip the one between. */
float64 guard = firedPostSec_;
if (config_.holdoffSec > guard) {
guard = config_.holdoffSec;
}
if (t >= (trigTime_ + guard)) {
pendingTime_ = t;
pendingValid_ = true;
}
}
else {
/* Already holding an edge, or no window latched to measure against. */
}
} }
mutex_.FastUnLock(); mutex_.FastUnLock();
@@ -197,7 +141,11 @@ bool TriggerEngine::Force() {
bool ok = lastTimeValid_ && (state_ != kTrigCollecting); bool ok = lastTimeValid_ && (state_ != kTrigCollecting);
if (ok) { if (ok) {
LatchWindowLocked(lastTime_); state_ = kTrigCollecting;
trigTime_ = lastTime_;
firedPreSec_ = config_.windowSec * config_.prePercent / 100.0;
firedPostSec_ = config_.windowSec - firedPreSec_;
firedValid_ = true;
REPORT_ERROR_STATIC(MARTe::ErrorManagement::Information, REPORT_ERROR_STATIC(MARTe::ErrorManagement::Information,
"TriggerEngine: forced at t=%.6f (pre=%.4fs post=%.4fs)", "TriggerEngine: forced at t=%.6f (pre=%.4fs post=%.4fs)",
trigTime_, firedPreSec_, firedPostSec_); trigTime_, firedPreSec_, firedPostSec_);
+3 -35
View File
@@ -88,24 +88,9 @@ public:
*/ */
uint32 GetConfigEpoch() const; uint32 GetConfigEpoch() const;
/** /** @brief Arm: any state → ARMED (resets edge detection). */
* @brief Arm: any state → ARMED (resets edge detection).
* This is the user's own arm, so it discards any edge remembered during the
* previous capture: the user asked for the next event, not for one that has
* already been and gone.
*/
void Arm(); void Arm();
/**
* @brief The automatic arm at the end of a capture (normal mode).
* Unlike Arm() it honours an edge seen while the capture was being
* collected, firing on it at once rather than waiting for the next one, and
* it keeps the tracked level so the first sample afterwards is compared
* against its real predecessor. TRIGGERED → COLLECTING when an edge was
* remembered, otherwise → ARMED.
*/
void Rearm();
/** @brief Disarm: any state → IDLE; clears the stopped flag. */ /** @brief Disarm: any state → IDLE; clears the stopped flag. */
void Disarm(); void Disarm();
@@ -117,10 +102,8 @@ public:
/** /**
* @brief Edge-detect one decoded sample of the configured signal. * @brief Edge-detect one decoded sample of the configured signal.
* Receive-thread context. In ARMED state a matching edge latches trigTime * Receive-thread context. Only acts in ARMED state; on a matching edge
* and the pre/post window and moves to COLLECTING. While a capture is in * latches trigTime and the pre/post window and moves to COLLECTING.
* flight (COLLECTING/TRIGGERED) the comparator keeps running and the first
* edge clear of that capture is remembered for the next Rearm().
*/ */
void CheckSample(float64 t, float64 v); void CheckSample(float64 t, float64 v);
@@ -160,21 +143,6 @@ private:
float64 firedPreSec_; ///< Window pre-part latched at fire time float64 firedPreSec_; ///< Window pre-part latched at fire time
float64 firedPostSec_; ///< Window post-part latched at fire time float64 firedPostSec_; ///< Window post-part latched at fire time
bool firedValid_; ///< true after a fire, until Disarm() bool firedValid_; ///< true after a fire, until Disarm()
/**
* The edge to fire on as soon as the FSM rearms, in sample time, recorded
* while a capture is still being collected or handed out. Without it the
* trigger is deaf from its own trigger point until the capture has been
* harvested and the holdoff has run, and then waits for a fresh edge, which
* on a sparse pulse train rounds the capture spacing up to a whole pulse
* period. Remembering the edge instead makes the blind stretch exactly the
* guard interval it has to be, since the capture is built from the edge's
* own timestamp and the rings still hold everything around it.
*/
float64 pendingTime_;
bool pendingValid_;
/** @brief Freeze the pre/post split at fire time; caller holds the mutex. */
void LatchWindowLocked(float64 t);
}; };
inline TriggerConfig::TriggerConfig() inline TriggerConfig::TriggerConfig()
@@ -51,8 +51,6 @@ UDPSourceSession::UDPSourceSession()
ringTemporal_(1000000u), ringTemporal_(1000000u),
ringScalar_(100000u), ringScalar_(100000u),
initialised_(false), initialised_(false),
client_(static_cast<MARTe::UDPSClientI *>(0)),
external_(false),
numSignals_(0u), numSignals_(0u),
publishMode_(0u), publishMode_(0u),
configured_(false), configured_(false),
@@ -101,8 +99,6 @@ void UDPSourceSession::ResetCalibration() {
lastPktWallValid_[i] = false; lastPktWallValid_[i] = false;
lastPktWallS_[i] = 0.0; lastPktWallS_[i] = 0.0;
accScalarPrevN_[i] = 0u; accScalarPrevN_[i] = 0u;
accScalarDtValid_[i] = false;
accScalarDtEMA_[i] = 0.0;
} }
} }
@@ -114,10 +110,7 @@ bool UDPSourceSession::Initialise(const char *id, const char *label,
const char *addr, uint16 port, const char *addr, uint16 port,
uint32 maxPts, uint32 maxPts,
const char *mcGroup, const char *mcGroup,
uint16 dataPort, uint16 dataPort) {
const char *transport,
const char *streamName,
MARTe::UDPSClientI *external) {
(void) Stop(); (void) Stop();
id_ = id; id_ = id;
@@ -126,42 +119,25 @@ bool UDPSourceSession::Initialise(const char *id, const char *label,
port_ = port; port_ = port;
mcGroup_ = (mcGroup != static_cast<const char *>(0)) ? mcGroup : ""; mcGroup_ = (mcGroup != static_cast<const char *>(0)) ? mcGroup : "";
dataPort_ = dataPort; dataPort_ = dataPort;
transport_ = ((transport != static_cast<const char *>(0)) && (strcmp(transport, "tcp") == 0)) ? "tcp" : "udp";
streamName_ = (streamName != static_cast<const char *>(0)) ? streamName : "";
maxPoints_ = (maxPts > 0u) ? maxPts : 20000u; maxPoints_ = (maxPts > 0u) ? maxPts : 20000u;
/* Build ConfigurationDatabase for UDPSClient */ /* Build ConfigurationDatabase for UDPSClient */
ConfigurationDatabase cfg; ConfigurationDatabase cfg;
(void) cfg.Write("ServerAddr", addr); (void) cfg.Write("ServerAddr", addr);
(void) cfg.Write("Port", static_cast<uint32>(port)); (void) cfg.Write("Port", static_cast<uint32>(port));
const bool useTcp = (transport_ == "tcp"); if ((mcGroup != static_cast<const char *>(0)) && (strlen(mcGroup) > 0u)) {
if (useTcp) {
if (streamName_.Size() > 0u) {
(void) cfg.Write("StreamName", streamName_.Buffer());
}
} else if ((mcGroup != static_cast<const char *>(0)) && (strlen(mcGroup) > 0u)) {
(void) cfg.Write("MulticastGroup", mcGroup); (void) cfg.Write("MulticastGroup", mcGroup);
if (dataPort > 0u) { if (dataPort > 0u) {
(void) cfg.Write("DataPort", static_cast<uint32>(dataPort)); (void) cfg.Write("DataPort", static_cast<uint32>(dataPort));
} }
} }
external_ = (external != static_cast<MARTe::UDPSClientI *>(0)); if (!client_.Initialise(cfg)) {
if (external_) {
client_ = external; /* already configured by its owner */
}
else {
client_ = useTcp ? static_cast<MARTe::UDPSClientI *>(&tcpClient_)
: static_cast<MARTe::UDPSClientI *>(&udpClient_);
}
if (!external_ && !client_->Initialise(cfg)) {
REPORT_ERROR_STATIC(MARTe::ErrorManagement::FatalError, REPORT_ERROR_STATIC(MARTe::ErrorManagement::FatalError,
"UDPSourceSession[%s]: Failed to initialise %s client.", "UDPSourceSession[%s]: Failed to initialise UDPSClient.", id);
id, useTcp ? "TCPSClient" : "UDPSClient");
client_ = static_cast<MARTe::UDPSClientI *>(0);
return false; return false;
} }
client_->SetListener(this); client_.SetListener(this);
(void) metaMutex_.FastLock(); (void) metaMutex_.FastLock();
numSignals_ = 0u; numSignals_ = 0u;
@@ -187,22 +163,12 @@ bool UDPSourceSession::Start() {
statByteCount_ = 0u; statByteCount_ = 0u;
stateStr_ = "connecting"; stateStr_ = "connecting";
statsMutex_.FastUnLock(); statsMutex_.FastUnLock();
return client_->Start(); return client_.Start();
}
void UDPSourceSession::DetachClient() {
if (!external_) { return; }
(void) Stop();
if (client_ != static_cast<MARTe::UDPSClientI *>(0)) {
client_->SetListener(static_cast<MARTe::UDPSClientListener *>(0));
}
client_ = static_cast<MARTe::UDPSClientI *>(0);
external_ = false;
} }
bool UDPSourceSession::Stop() { bool UDPSourceSession::Stop() {
if (!initialised_ || (client_ == static_cast<MARTe::UDPSClientI *>(0))) { return true; } if (!initialised_) { return true; }
return client_->Stop(); return client_.Stop();
} }
/*---------------------------------------------------------------------------*/ /*---------------------------------------------------------------------------*/
@@ -231,8 +197,7 @@ void UDPSourceSession::OnUDPSConfig(const uint8 *payload, uint32 payloadSize) {
} }
void UDPSourceSession::OnUDPSData(const uint8 *payload, uint32 payloadSize) { void UDPSourceSession::OnUDPSData(const uint8 *payload, uint32 payloadSize) {
/* Valid only for the duration of this callback. */ ParseDataPayload(payload, payloadSize);
ParseDataPayload(payload, payloadSize, client_->GetLastDataGap());
} }
/*---------------------------------------------------------------------------*/ /*---------------------------------------------------------------------------*/
@@ -260,9 +225,6 @@ void UDPSourceSession::ParseConfigPayload(const uint8 *payload, uint32 size) {
sigDescs_[i].unit[sizeof(sigDescs_[i].unit) - 1u] = '\0'; sigDescs_[i].unit[sizeof(sigDescs_[i].unit) - 1u] = '\0';
} }
publishMode_ = payload[4u + numSigs * UDPS_SIGNAL_DESC_SIZE]; publishMode_ = payload[4u + numSigs * UDPS_SIGNAL_DESC_SIZE];
hrtFreq_ = UDPSConfigHrtFrequency(
payload, size, numSigs,
static_cast<float64>(MARTe::HighResolutionTimer::Frequency()));
numSignals_ = numSigs; numSignals_ = numSigs;
configured_ = true; configured_ = true;
@@ -311,9 +273,8 @@ void UDPSourceSession::ParseConfigPayload(const uint8 *payload, uint32 size) {
} }
REPORT_ERROR_STATIC(MARTe::ErrorManagement::Information, REPORT_ERROR_STATIC(MARTe::ErrorManagement::Information,
"UDPSourceSession[%s]: CONFIG received — %u signals, " "UDPSourceSession[%s]: CONFIG received — %u signals.",
"producer HRT %.0f Hz.", id_.Buffer(), numSigs);
id_.Buffer(), numSigs, hrtFreq_);
} }
void UDPSourceSession::AllocateRingBuffers() { void UDPSourceSession::AllocateRingBuffers() {
@@ -415,8 +376,7 @@ float64 UDPSourceSession::ProducerNewestTime() const {
/* DATA parsing */ /* DATA parsing */
/*---------------------------------------------------------------------------*/ /*---------------------------------------------------------------------------*/
void UDPSourceSession::ParseDataPayload(const uint8 *payload, uint32 size, void UDPSourceSession::ParseDataPayload(const uint8 *payload, uint32 size) {
uint32 lostPackets) {
if (size < 8u) { return; } if (size < 8u) { return; }
/* Copy metadata under lock */ /* Copy metadata under lock */
@@ -608,9 +568,9 @@ void UDPSourceSession::ParseDataPayload(const uint8 *payload, uint32 size,
* immune to this because it is sampled at acquisition. * immune to this because it is sampled at acquisition.
* *
* hrtTimestamp is the HRT counter of sample 0; hrtFreq_ (the * hrtTimestamp is the HRT counter of sample 0; hrtFreq_ (the
* producer's tick rate, taken from the CONFIG trailer) converts * local HRT frequency, identical to the sender on the same
* it to seconds, then a one-time calibration maps the sender * host) converts it to seconds, then a one-time calibration
* clock onto wall-clock. */ * maps the sender clock onto wall-clock. */
const float64 hrt0Sec = static_cast<float64>(hrtTimestamp) / const float64 hrt0Sec = static_cast<float64>(hrtTimestamp) /
hrtFreq_; hrtFreq_;
if ((!timeSigCalibValid_[s]) || if ((!timeSigCalibValid_[s]) ||
@@ -621,25 +581,16 @@ void UDPSourceSession::ParseDataPayload(const uint8 *payload, uint32 size,
} }
/* Per-sample dt: samplingRate if present, else derive it from /* Per-sample dt: samplingRate if present, else derive it from
* the sender-HRT gap to the previous packet. * the sender-HRT gap to the previous packet divided by that
* * packet's sample count (the flushes carry contiguous RT
* The gap is divided by the number of RT cycles it actually * cycles, so this is exactly one cycle period). */
* spans, not by the previous packet's sample count. Those two
* agree only while nothing is lost; once a packet goes missing
* the gap covers cycles the previous count never saw, and
* dividing by that count inflates dt until this packet's
* samples overrun into the next packet's range. lostPackets
* comes from the producer's packet counter, so the divisor
* widens with the gap and dt is unchanged. */
float64 dt; float64 dt;
if (desc.samplingRate > 0.0) { if (desc.samplingRate > 0.0) {
dt = 1.0 / desc.samplingRate; dt = 1.0 / desc.samplingRate;
} else if (lastPktWallValid_[s] && (accScalarPrevN_[s] > 0u) && } else if (lastPktWallValid_[s] && (accScalarPrevN_[s] > 0u) &&
(hrt0Sec > lastPktWallS_[s])) { (hrt0Sec > lastPktWallS_[s])) {
dt = UDPSEstimateAccumDt(hrt0Sec - lastPktWallS_[s], dt = (hrt0Sec - lastPktWallS_[s]) /
accScalarPrevN_[s], lostPackets, static_cast<float64>(accScalarPrevN_[s]);
accScalarDtEMA_[s],
accScalarDtValid_[s]);
} else { } else {
dt = 1.0e-3; /* 1 kHz default until the gap is known */ dt = 1.0e-3; /* 1 kHz default until the gap is known */
} }
@@ -979,10 +930,6 @@ StreamString UDPSourceSession::GetMulticastGroup() const { return mcGroup_; }
uint16 UDPSourceSession::GetDataPort() const { return dataPort_; } uint16 UDPSourceSession::GetDataPort() const { return dataPort_; }
StreamString UDPSourceSession::GetTransport() const { return transport_; }
StreamString UDPSourceSession::GetStreamName() const { return streamName_; }
bool UDPSourceSession::IsConfigured() const { bool UDPSourceSession::IsConfigured() const {
(void) metaMutex_.FastLock(); (void) metaMutex_.FastLock();
bool c = configured_; bool c = configured_;
@@ -11,7 +11,6 @@
#define STREAMHUB_UDP_SOURCE_SESSION_H_ #define STREAMHUB_UDP_SOURCE_SESSION_H_
#include "UDPSClient.h" #include "UDPSClient.h"
#include "TCPSClient.h"
#include "UDPSProtocol.h" #include "UDPSProtocol.h"
#include "FastPollingMutexSem.h" #include "FastPollingMutexSem.h"
#include "HighResolutionTimer.h" #include "HighResolutionTimer.h"
@@ -40,104 +39,6 @@ using MARTe::ConfigurationDatabase;
/** Maximum number of signals per source session. */ /** Maximum number of signals per source session. */
static const uint32 UDPSS_MAX_SIGNALS = 256u; static const uint32 UDPSS_MAX_SIGNALS = 256u;
/* Accumulated-scalar dt estimator tuning. */
/** Weight of a new observation; slow enough that one bad gap barely moves it. */
static const float64 UDPSS_DT_EMA_ALPHA = 0.05;
/** Observations outside [lo, hi] x the current estimate are treated as a
* mis-counted gap and discarded rather than smoothed in. */
static const float64 UDPSS_DT_ACCEPT_LO = 0.5;
static const float64 UDPSS_DT_ACCEPT_HI = 2.0;
/**
* @brief Per-sample period of an accumulated scalar packet, robust to loss.
*
* An Accumulate producer batches consecutive RT cycles, so the sender-clock
* gap between two packets' first samples covers exactly as many cycles as the
* earlier packet carried — but only while nothing is lost in between. Over UDP
* (and with a producer that can overwrite a batch the sender never took) that
* assumption fails, and dividing the gap by the previous packet's sample count
* then inflates the period. The packet's own samples are laid out as
* base + e*dt, so an inflated dt walks them past their real end and into the
* span the next packet will claim: samples collide there and leave a hole
* behind them.
*
* The number of packets that went missing is not guessed from the gap — that
* is circular, and an estimator that infers the cycle count from its own
* period has a stable fixed point wherever gap/dt is an integer, so a genuine
* rate change locks it at the old period forever. It comes instead from the
* UDPS packet counter, which the producer increments once per sent packet. The
* gap then spans (1 + lost) batches, each assumed to be prevN cycles, and with
* nothing lost the formula reduces exactly to gap/prevN.
*
* @param gap Sender-clock seconds since the previous packet's first sample.
* Must be > 0.
* @param prevN Samples in the previous packet. Must be > 0.
* @param lost Packets missing between the previous packet and this one,
* from the producer's counter.
* @param[in,out] dtEMA Smoothed period. Seeded on the first call.
* @param[in,out] dtValid False until dtEMA holds an estimate.
* @return The period to space this packet's samples by.
*/
inline float64 UDPSEstimateAccumDt(const float64 gap, const uint32 prevN,
const uint32 lost, float64 &dtEMA,
bool &dtValid) {
float64 cycles = static_cast<float64>(prevN) *
(1.0 + static_cast<float64>(lost));
if (cycles < 1.0) {
cycles = 1.0;
}
const float64 dtObs = gap / cycles;
if (!dtValid) {
dtEMA = dtObs;
dtValid = true;
} else if ((dtObs > (dtEMA * UDPSS_DT_ACCEPT_LO)) &&
(dtObs < (dtEMA * UDPSS_DT_ACCEPT_HI))) {
/* Track slow drift, but ignore observations far outside the current
* estimate: those are the signature of a mis-counted gap, and folding
* one in would drag the estimate towards the very error it exists to
* absorb. */
dtEMA = ((1.0 - UDPSS_DT_EMA_ALPHA) * dtEMA) +
(UDPSS_DT_EMA_ALPHA * dtObs);
}
return dtEMA;
}
/**
* @brief Pick the tick rate to divide a producer's DATA timestamps by.
*
* DATA packets carry the raw value of the producer's high-resolution counter,
* which is meaningless without the rate it runs at. The rate is published in
* the CONFIG trailer, after the descriptors and the publish-mode byte. When it
* is missing — an older producer, or one that could not determine it — the
* only remaining option is this host's own timer, which is right only while
* the two machines agree; on x86 that is the TSC frequency, so it is a
* different number on every model.
*
* @param payload Reassembled CONFIG payload.
* @param size Bytes in @p payload.
* @param numSigs Signal count already read from the payload, capped to what
* the receiver will store.
* @param localFreq This host's HRT frequency, used as the fallback.
* @return Ticks per second to convert DATA timestamps with; never 0.
*/
inline float64 UDPSConfigHrtFrequency(const uint8 *payload, const uint32 size,
const uint32 numSigs,
const float64 localFreq) {
const uint32 offset = 4u + (numSigs * MARTe::UDPS_SIGNAL_DESC_SIZE) + 1u;
if ((payload != NULL_PTR(const uint8 *)) && (size >= (offset + 8u))) {
uint64 wireFreq = 0u;
memcpy(&wireFreq, payload + offset, 8u);
/* Anything below 1 kHz is not a high-resolution timer; the field is
* either absent, unset, or the payload was mis-parsed, and adopting it
* would stretch every timestamp far enough to make the trace useless. */
if (wireFreq >= 1000u) {
return static_cast<float64>(wireFreq);
}
}
return localFreq;
}
/** /**
* @brief One connected UDPStreamer source. * @brief One connected UDPStreamer source.
* *
@@ -159,23 +60,11 @@ public:
* @param maxPts Ring buffer capacity per signal. * @param maxPts Ring buffer capacity per signal.
* @param mcGroup Multicast group (empty string = unicast). * @param mcGroup Multicast group (empty string = unicast).
* @param dataPort Multicast data port (0 = port+1). * @param dataPort Multicast data port (0 = port+1).
* @param transport "udp" (default) or "tcp". TCP ignores mcGroup/dataPort.
* @param streamName TCP only: stream to subscribe to on a TCPStreamService
* ("" = plain TCPStreamer, no subscription).
* @param external Optional receiver owned by the caller (one stream of a
* multiplexed TCPSClient, see TCPSClient::GetStream()). When set the
* session does not create or configure its own client; call
* DetachClient() before the receiver is destroyed.
* @return true on success. * @return true on success.
*/ */
bool Initialise(const char *id, const char *label, bool Initialise(const char *id, const char *label,
const char *addr, uint16 port, uint32 maxPts, const char *addr, uint16 port, uint32 maxPts,
const char *mcGroup = "", uint16 dataPort = 0u, const char *mcGroup = "", uint16 dataPort = 0u);
const char *transport = "udp", const char *streamName = "",
MARTe::UDPSClientI *external = static_cast<MARTe::UDPSClientI *>(0));
/** @brief Stop and forget an external receiver (no-op for built-in receivers). */
void DetachClient();
/** @brief Start the UDPSClient background thread. */ /** @brief Start the UDPSClient background thread. */
bool Start(); bool Start();
@@ -249,12 +138,6 @@ public:
/** @return Multicast data port (0 = port+1 default). */ /** @return Multicast data port (0 = port+1 default). */
uint16 GetDataPort() const; uint16 GetDataPort() const;
/** @return "udp" or "tcp". */
StreamString GetTransport() const;
/** @return TCP stream name ("" = none). */
StreamString GetStreamName() const;
/** @return true after the first CONFIG packet has been fully parsed. */ /** @return true after the first CONFIG packet has been fully parsed. */
bool IsConfigured() const; bool IsConfigured() const;
@@ -346,13 +229,7 @@ private:
/* DATA payload parsing */ /* DATA payload parsing */
void ParseConfigPayload(const uint8 *payload, uint32 size); void ParseConfigPayload(const uint8 *payload, uint32 size);
/** void ParseDataPayload(const uint8 *payload, uint32 size);
* @param lostPackets DATA packets missing immediately before this one, from
* the producer's counter; the accumulated-scalar period estimate
* needs it to know how many cycles the sender-clock gap spans.
*/
void ParseDataPayload(const uint8 *payload, uint32 size,
uint32 lostPackets);
void AllocateRingBuffers(); void AllocateRingBuffers();
/** @brief Invalidate all wall-clock calibration state (receive thread only). */ /** @brief Invalidate all wall-clock calibration state (receive thread only). */
@@ -462,18 +339,13 @@ private:
uint16 port_; uint16 port_;
StreamString mcGroup_; ///< Multicast group ("" = unicast) StreamString mcGroup_; ///< Multicast group ("" = unicast)
uint16 dataPort_; ///< Multicast data port (0 = port+1) uint16 dataPort_; ///< Multicast data port (0 = port+1)
StreamString transport_; ///< "udp" or "tcp"
StreamString streamName_;///< TCPStreamService stream name ("" = none)
uint32 maxPoints_; uint32 maxPoints_;
uint32 ringTemporal_; ///< Ring capacity for multi-element signals uint32 ringTemporal_; ///< Ring capacity for multi-element signals
uint32 ringScalar_; ///< Ring capacity for scalar signals uint32 ringScalar_; ///< Ring capacity for scalar signals
bool initialised_; bool initialised_;
/* Receivers (each owns a background thread); client_ points at the active one. */ /* UDPSClient (owns the background thread) */
UDPSClient udpClient_; UDPSClient client_;
MARTe::TCPSClient tcpClient_;
MARTe::UDPSClientI *client_;
bool external_; ///< client_ is owned by the caller
/* Signal metadata — protected by metaMutex_ */ /* Signal metadata — protected by metaMutex_ */
mutable FastPollingMutexSem metaMutex_; mutable FastPollingMutexSem metaMutex_;
@@ -522,20 +394,13 @@ private:
float64 lastPktWallS_[UDPSS_MAX_SIGNALS]; float64 lastPktWallS_[UDPSS_MAX_SIGNALS];
bool lastPktWallValid_[UDPSS_MAX_SIGNALS]; bool lastPktWallValid_[UDPSS_MAX_SIGNALS];
/* Accumulated-scalar timing: the producer's HRT counter frequency and the /* Accumulated-scalar timing: HRT counter frequency (local == sender on the
* previous packet's sample count, used to reconstruct per-sample timestamps * same host) and the previous packet's sample count, used to reconstruct
* from the embedded sender HRT instead of the (UDP burst-sensitive) packet * per-sample timestamps from the embedded sender HRT instead of the (UDP
* arrival time. Seeded from this host's timer and replaced by the rate the * burst-sensitive) packet arrival time. */
* producer publishes in CONFIG; see UDPSConfigHrtFrequency. */
float64 hrtFreq_; float64 hrtFreq_;
uint32 accScalarPrevN_[UDPSS_MAX_SIGNALS]; uint32 accScalarPrevN_[UDPSS_MAX_SIGNALS];
/* Per-signal state of UDPSEstimateAccumDt (see above): the smoothed
* per-sample period for accumulated scalars whose descriptor carries no
* SamplingRate. */
float64 accScalarDtEMA_[UDPSS_MAX_SIGNALS];
bool accScalarDtValid_[UDPSS_MAX_SIGNALS];
/* Scratch buffers for decoding arrays (receive thread only). */ /* Scratch buffers for decoding arrays (receive thread only). */
float64 *timeScratch_; ///< Time values scratch float64 *timeScratch_; ///< Time values scratch
float64 *valScratch_; ///< Data values scratch float64 *valScratch_; ///< Data values scratch
@@ -1,25 +0,0 @@
#############################################################
#
# 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
#
# 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 for the specific language governing
# permissions and limitations under the Licence.
#
#############################################################
include Makefile.inc
@@ -1,63 +0,0 @@
#############################################################
#
# 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
#
# 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 for the specific language governing
# permissions and limitations under the Licence.
#
#############################################################
OBJSX = TCPStreamer.x
PACKAGE=Components/DataSources
ROOT_DIR=../../../../
MAKEDEFAULTDIR=$(MARTe2_DIR)/MakeDefaults
include $(MAKEDEFAULTDIR)/MakeStdLibDefs.$(TARGET)
INCLUDES += -I.
INCLUDES += -I$(ROOT_DIR)/Common/UDP
INCLUDES += -I$(ROOT_DIR)/Source/Components/Interfaces/UDPStream
INCLUDES += -I$(ROOT_DIR)/Source/Components/DataSources/UDPStreamer
INCLUDES += -I$(ROOT_DIR)/Source/Components/Interfaces/TCPStreamService
INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L0Types
INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L1Portability
INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L2Objects
INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L3Streams
INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L4Messages
INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L4Configuration
INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L5GAMs
INCLUDES += -I$(MARTe2_DIR)/Source/Core/Scheduler/L1Portability
INCLUDES += -I$(MARTe2_DIR)/Source/Core/Scheduler/L3Services
INCLUDES += -I$(MARTe2_DIR)/Source/Core/Scheduler/L4Messages
INCLUDES += -I$(MARTe2_DIR)/Source/Core/Scheduler/L5GAMs
INCLUDES += -I$(MARTe2_DIR)/Source/Core/FileSystem/L1Portability
INCLUDES += -I$(MARTe2_DIR)/Source/Core/FileSystem/L3Streams
LIBRARIES += -L$(BUILD_DIR)/../../Interfaces/UDPStream -lUDPStream
LIBRARIES += -L$(ROOT_DIR)/Build/$(TARGET)/Components/Interfaces/TCPStreamService -lTCPStreamService
LIBRARIES += -L$(ROOT_DIR)/Build/$(TARGET)/Components/DataSources/UDPStreamer -lUDPStreamer
all: $(OBJS) \
$(BUILD_DIR)/TCPStreamer$(LIBEXT) \
$(BUILD_DIR)/TCPStreamer$(DLLEXT)
echo $(OBJS)
include depends.$(TARGET)
include $(MAKEDEFAULTDIR)/MakeStdLibRules.$(TARGET)
@@ -1,172 +0,0 @@
/**
* @file TCPStreamer.cpp
* @brief Source file for class TCPStreamer
* @date 05/11/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 source file contains the definition of all the methods for
* the class TCPStreamer (public, protected, and private).
*/
#define DLL_API
#include <string.h>
#include "AdvancedErrorManagement.h"
#include "ConfigurationDatabase.h"
#include "TCPStreamer.h"
namespace MARTe {
/* Owned by the TCPStreamService when Service is set. */
static const char8 *const tcpStreamerServiceKeys[] = {
"Port", "MaxClients", "ClientQueueDepth", "ClientStallTimeout", "SendBufferSize"
};
static const char8 *const tcpStreamerRejectedKeys[] = {
"MulticastGroup", "Interface", "DataPort", "ClientTimeout"
};
TCPStreamer::TCPStreamer() : UDPStreamer(), serviceStream(NULL_PTR(TCPSServiceStream *)) {
tcpServer = new TCPSServer();
SetServer(tcpServer);
}
TCPStreamer::~TCPStreamer() {
/* tcpServer is owned (and deleted) by UDPStreamer. */
tcpServer = NULL_PTR(TCPSServer *);
serviceStream = NULL_PTR(TCPSServiceStream *);
}
bool TCPStreamer::Initialise(StructuredDataI &data) {
bool ok = true;
const uint32 numKeys = data.GetNumberOfChildren();
for (uint32 i = 0u; (i < numKeys) && ok; i++) {
const char8 *name = data.GetChildName(i);
if (name == NULL_PTR(const char8 *)) {
continue;
}
for (uint32 k = 0u; k < (sizeof(tcpStreamerRejectedKeys) / sizeof(tcpStreamerRejectedKeys[0])); k++) {
if (strcmp(name, tcpStreamerRejectedKeys[k]) == 0) {
REPORT_ERROR(ErrorManagement::ParametersError,
"Parameter '%s' is UDP-only and not supported by TCPStreamer.", name);
ok = false;
}
}
}
StreamString serviceName;
if (ok && data.Read("Service", serviceName) && (serviceName.Size() > 0u)) {
for (uint32 i = 0u; (i < numKeys) && ok; i++) {
const char8 *name = data.GetChildName(i);
for (uint32 k = 0u; (name != NULL_PTR(const char8 *)) &&
(k < (sizeof(tcpStreamerServiceKeys) / sizeof(tcpStreamerServiceKeys[0]))); k++) {
if (strcmp(name, tcpStreamerServiceKeys[k]) == 0) {
REPORT_ERROR(ErrorManagement::ParametersError,
"Parameter '%s' belongs to the TCPStreamService and cannot be set together with Service.",
name);
ok = false;
}
}
}
StreamString streamName;
if (ok && (!data.Read("StreamName", streamName) || (streamName.Size() == 0u))) {
streamName = GetName();
}
if (ok && (streamName.Size() > TCPS_MAX_STREAM_NAME)) {
REPORT_ERROR(ErrorManagement::ParametersError,
"StreamName is longer than %u characters.", TCPS_MAX_STREAM_NAME);
ok = false;
}
if (ok) {
serviceStream = new TCPSServiceStream(serviceName.Buffer(), streamName.Buffer());
SetServer(serviceStream); /* replaces (and deletes) the private TCPSServer */
tcpServer = NULL_PTR(TCPSServer *);
}
}
if (ok) {
ok = UDPStreamer::Initialise(data);
}
return ok;
}
bool TCPStreamer::UsesService() const {
return serviceStream != NULL_PTR(TCPSServiceStream *);
}
uint16 TCPStreamer::GetBoundPort() const {
return UsesService() ? serviceStream->GetPort() : tcpServer->GetPort();
}
uint32 TCPStreamer::GetClientCount() const {
return UsesService() ? serviceStream->GetClientCount() : tcpServer->GetClientCount();
}
uint64 TCPStreamer::GetClientDrops() const {
return UsesService() ? serviceStream->GetClientDrops() : tcpServer->GetClientDrops();
}
uint32 TCPStreamer::GetStallEvictions() const {
return UsesService() ? 0u : tcpServer->GetStallEvictions();
}
uint32 TCPStreamer::DefaultMaxPayloadSize() const {
return TCPSServer::TCPS_DEFAULT_MAX_PAYLOAD;
}
bool TCPStreamer::InitialiseTransport(StructuredDataI &data) {
if (UsesService()) {
ConfigurationDatabase scdb;
bool sok = scdb.Write("MaxPayloadSize", GetMaxPayloadSize());
if (sok) {
sok = scdb.MoveToRoot();
}
if (sok) {
sok = serviceStream->Initialise(scdb);
}
if (!sok) {
REPORT_ERROR(ErrorManagement::ParametersError, "Could not configure the service stream.");
}
return sok;
}
ConfigurationDatabase cdb;
/* The base class has already resolved Port and MaxPayloadSize. */
bool ok = cdb.Write("Port", static_cast<uint32>(GetPort()));
if (ok) {
ok = cdb.Write("MaxPayloadSize", GetMaxPayloadSize());
}
const char8 *const optional[] = {
"MaxClients", "ClientQueueDepth", "ClientStallTimeout", "SendBufferSize"
};
for (uint32 k = 0u; (k < (sizeof(optional) / sizeof(optional[0]))) && ok; k++) {
uint32 v = 0u;
if (data.Read(optional[k], v)) {
ok = cdb.Write(optional[k], v);
}
}
if (ok) {
ok = cdb.MoveToRoot();
}
if (ok) {
ok = tcpServer->Initialise(cdb);
}
if (!ok) {
REPORT_ERROR(ErrorManagement::ParametersError, "Could not configure the TCPSServer.");
}
return ok;
}
CLASS_REGISTER(TCPStreamer, "1.0")
} /* namespace MARTe */
@@ -1,112 +0,0 @@
/**
* @file TCPStreamer.h
* @brief Header file for class TCPStreamer
* @date 05/11/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 TCPStreamer
* with all of its public, protected and private members.
*/
#ifndef TCPSTREAMER_H_
#define TCPSTREAMER_H_
#include "TCPSServer.h"
#include "TCPSServiceStream.h"
#include "UDPStreamer.h"
namespace MARTe {
/**
* @brief A DataSource that streams MARTe2 signals to TCP clients.
*
* @details Identical to UDPStreamer (same signals, quantisation, time modes and
* PublishingMode handling, same UDPS wire format) except that the network side
* is a TCPSServer: every client gets the CONFIG on connect and then every DATA
* frame, in order, over its own TCP connection. A client that cannot keep up
* only loses its own frames (counted by GetClientDrops()) and is disconnected
* after ClientStallTimeout seconds without progress; the RT thread and the
* other clients are unaffected.
*
* @par Configuration
* All UDPStreamer keys apply except the UDP-only ones (MulticastGroup,
* Interface, DataPort, ClientTimeout), which are rejected.
* | Parameter | Type | Default | Description |
* |--------------------|--------|---------|-------------|
* | Port | uint16 | 44500 | TCP listen port. |
* | MaxPayloadSize | uint32 | 60000 | Frame budget used to size Accumulate batches. |
* | MaxClients | uint32 | 16 | Simultaneous clients (1..1024). |
* | ClientQueueDepth | uint32 | 8 | DATA frames queued per client (1..4096). |
* | ClientStallTimeout | uint32 | 5 | Seconds without progress before a client is dropped; 0 disables. |
* | SendBufferSize | uint32 | 0 | SO_SNDBUF for client sockets; 0 keeps the OS default. |
*
* @par Shared service mode
* With `Service = <name of a TCPStreamService>` the DataSource does not open a
* socket of its own: it publishes a stream called `StreamName` (default: the
* DataSource name) through the shared in-process service, which serves many
* streams from one port with a pool of worker threads. Clients select the
* stream with the `StreamName` key of TCPStreamerClient / StreamHub. In this
* mode Port, MaxClients, ClientQueueDepth, ClientStallTimeout and
* SendBufferSize belong to the service and are rejected here.
* | Parameter | Type | Default | Description |
* |------------|--------|-----------------|-------------|
* | Service | string | *(absent)* | Object name of the TCPStreamService. |
* | StreamName | string | DataSource name | Stream name, unique per service (1..255 chars). |
*
* @par Example
* <pre>
* +Streamer = {
* Class = TCPStreamer
* Port = 44500
* Signals = {
* Time = { Type = uint64 }
* Value = { Type = float32 }
* }
* }
* </pre>
*/
class TCPStreamer : public UDPStreamer {
public:
CLASS_REGISTER_DECLARATION()
TCPStreamer();
virtual ~TCPStreamer();
/** Rejects the UDP-only keys, then behaves as UDPStreamer::Initialise(). */
virtual bool Initialise(StructuredDataI &data);
/** @return true when publishing through a TCPStreamService. */
bool UsesService() const;
/** @return the port actually bound (meaningful once started; resolves Port = 0). */
uint16 GetBoundPort() const;
/** @return clients currently connected. */
uint32 GetClientCount() const;
/** @return DATA frames dropped across all clients because their queue was full. */
uint64 GetClientDrops() const;
/** @return clients disconnected for exceeding ClientStallTimeout. */
uint32 GetStallEvictions() const;
protected:
virtual uint32 DefaultMaxPayloadSize() const;
virtual bool InitialiseTransport(StructuredDataI &data);
private:
TCPSServer *tcpServer; /**< Alias of the private transport (NULL in service mode). */
TCPSServiceStream *serviceStream; /**< Alias of the service handle (NULL in standalone mode). */
};
} /* namespace MARTe */
#endif /* TCPSTREAMER_H_ */
@@ -1,145 +0,0 @@
../../../..//Build/x86-linux/Components/DataSources/TCPStreamer/TCPStreamer.o: TCPStreamer.cpp \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/AdvancedErrorManagement.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ClassProperties.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/GeneralDefinitions.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/TypeCharacteristics.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/CompilerTypes.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/Architecture/x86_gcc/CompilerTypes.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/GeneralDefinitions.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/FractionalInteger.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/TypeCharacteristics.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/TemplateParametersVerificator.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/CompilerTypes.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/ErrorManagement.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/ErrorInformation.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/ErrorType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BitBoolean.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BitRange.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/GeneralDefinitions.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StreamI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/TimeoutType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/HighResolutionTimer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/TimeStamp.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Architecture/x86_gcc/HighResolutionTimerA.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/HighResolutionTimer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/../../HighResolutionTimer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/HighResolutionTimerCalibrator.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/../../GeneralDefinitions.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/TimeStamp.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/StreamMemoryReference.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/BufferedStreamI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/TimeoutType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/AnyType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ClassProperties.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ClassRegistryDatabase.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/GlobalObjectsDatabase.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/GlobalObjectI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/StandardHeap.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/../../HeapI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/../../ErrorManagement.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/../../GeneralDefinitions.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/../Generic/StandardHeap_Generic.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/FastPollingMutexSem.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Atomic.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Architecture/x86_gcc/AtomicA.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/ErrorManagement.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Sleep.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ClassRegistryItem.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/CString.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/ZeroTerminatedArray.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/Introspection.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/IntrospectionEntry.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/TypeDescriptor.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BasicType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/FractionalInteger.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BitRange.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BitBoolean.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/ZeroTerminatedArray.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/LinkedListable.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/LinkedListHolderT.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/IteratorT.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/Iterator.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/LinkedListable.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/LinkedListHolder.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/SortFilter.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/SearchFilter.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/LoadableLibrary.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ObjectBuilder.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/HeapI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StaticList.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StaticListHolder.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/Matrix.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/Vector.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/HeapManager.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/HeapI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/MemoryOperationsHelper.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/FormatDescriptor.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/IOBuffer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/CharBuffer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StreamI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L4Configuration/ConfigurationDatabase.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L4Configuration/AnyObject.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/Object.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StringHelper.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/StructuredDataI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/AnyType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/CLASSREGISTER.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ClassRegistryItemT.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ObjectBuilderT.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L4Configuration/ConfigurationDatabaseNode.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/Fnv1aHashFunction.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/HashFunction.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ReferenceContainer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/LinkedListHolder.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/Object.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/Reference.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ReferenceContainerFilter.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ReferenceContainerNode.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ReferenceT.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/UnorderedMap.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/HeapManager.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StringHelper.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ReferenceContainerFilterObjectName.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ReferenceContainer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/StructuredDataI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L4Configuration/TypeConversion.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/Vector.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/StreamString.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/StreamStringIOBuffer.h \
TCPStreamer.h \
../../../..//Source/Components/Interfaces/UDPStream/TCPSServer.h \
../../../..//Common/UDP/UDPSProtocol.h \
../../../..//Source/Components/Interfaces/UDPStream/UDPSTransportI.h \
../../../..//Source/Components/Interfaces/TCPStreamService/TCPSServiceStream.h \
../../../..//Source/Components/Interfaces/TCPStreamService/TCPStreamService.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/EmbeddedServiceMethodBinderI.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/ExecutionInfo.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L1Portability/Threads.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/ThreadsB.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L1Portability/ExceptionHandler.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/ProcessorType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BitSet.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/ExecutionInfo.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/SingleThreadService.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/EmbeddedServiceI.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/EmbeddedServiceMethodBinderI.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/EmbeddedServiceMethodBinderT.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/EmbeddedThreadI.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/EmbeddedThread.h \
../../../..//Source/Components/Interfaces/UDPStream/UDPSTransportI.h \
../../../..//Source/Components/DataSources/UDPStreamer/UDPStreamer.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L1Portability/EventSem.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L5GAMs/MemoryDataSourceI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L5GAMs/DataSourceI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L5GAMs/StatefulI.h \
../../../..//Source/Components/Interfaces/UDPStream/UDPSServer.h \
/home/martino/workspace/MARTe2/Source/Core/FileSystem/L1Portability/BasicTCPSocket.h \
/home/martino/workspace/MARTe2/Source/Core/FileSystem/L1Portability/BasicSocket.h \
/home/martino/workspace/MARTe2/Source/Core/FileSystem/L1Portability/InternetHost.h \
/home/martino/workspace/MARTe2/Source/Core/FileSystem/L1Portability/Environment/Linux/InternetHostCore.h \
/home/martino/workspace/MARTe2/Source/Core/FileSystem/L1Portability/Environment/Linux/InternetMulticastCore.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/HandleI.h \
/home/martino/workspace/MARTe2/Source/Core/FileSystem/L1Portability/Environment/Linux/SocketCore.h \
/home/martino/workspace/MARTe2/Source/Core/FileSystem/L1Portability/BasicUDPSocket.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Sleep.h \
/home/martino/workspace/MARTe2/Source/Core/FileSystem/L1Portability/InternetHost.h
@@ -1,145 +0,0 @@
TCPStreamer.o: TCPStreamer.cpp \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/AdvancedErrorManagement.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ClassProperties.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/GeneralDefinitions.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/TypeCharacteristics.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/CompilerTypes.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/Architecture/x86_gcc/CompilerTypes.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/GeneralDefinitions.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/FractionalInteger.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/TypeCharacteristics.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/TemplateParametersVerificator.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/CompilerTypes.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/ErrorManagement.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/ErrorInformation.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/ErrorType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BitBoolean.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BitRange.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/GeneralDefinitions.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StreamI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/TimeoutType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/HighResolutionTimer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/TimeStamp.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Architecture/x86_gcc/HighResolutionTimerA.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/HighResolutionTimer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/../../HighResolutionTimer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/HighResolutionTimerCalibrator.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/../../GeneralDefinitions.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/TimeStamp.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/StreamMemoryReference.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/BufferedStreamI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/TimeoutType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/AnyType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ClassProperties.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ClassRegistryDatabase.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/GlobalObjectsDatabase.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/GlobalObjectI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/StandardHeap.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/../../HeapI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/../../ErrorManagement.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/../../GeneralDefinitions.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/../Generic/StandardHeap_Generic.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/FastPollingMutexSem.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Atomic.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Architecture/x86_gcc/AtomicA.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/ErrorManagement.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Sleep.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ClassRegistryItem.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/CString.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/ZeroTerminatedArray.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/Introspection.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/IntrospectionEntry.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/TypeDescriptor.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BasicType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/FractionalInteger.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BitRange.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BitBoolean.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/ZeroTerminatedArray.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/LinkedListable.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/LinkedListHolderT.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/IteratorT.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/Iterator.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/LinkedListable.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/LinkedListHolder.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/SortFilter.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/SearchFilter.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/LoadableLibrary.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ObjectBuilder.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/HeapI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StaticList.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StaticListHolder.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/Matrix.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/Vector.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/HeapManager.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/HeapI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/MemoryOperationsHelper.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/FormatDescriptor.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/IOBuffer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/CharBuffer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StreamI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L4Configuration/ConfigurationDatabase.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L4Configuration/AnyObject.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/Object.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StringHelper.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/StructuredDataI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/AnyType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/CLASSREGISTER.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ClassRegistryItemT.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ObjectBuilderT.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L4Configuration/ConfigurationDatabaseNode.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/Fnv1aHashFunction.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/HashFunction.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ReferenceContainer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/LinkedListHolder.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/Object.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/Reference.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ReferenceContainerFilter.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ReferenceContainerNode.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ReferenceT.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/UnorderedMap.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/HeapManager.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StringHelper.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ReferenceContainerFilterObjectName.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ReferenceContainer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/StructuredDataI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L4Configuration/TypeConversion.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/Vector.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/StreamString.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/StreamStringIOBuffer.h \
TCPStreamer.h \
../../../..//Source/Components/Interfaces/UDPStream/TCPSServer.h \
../../../..//Common/UDP/UDPSProtocol.h \
../../../..//Source/Components/Interfaces/UDPStream/UDPSTransportI.h \
../../../..//Source/Components/Interfaces/TCPStreamService/TCPSServiceStream.h \
../../../..//Source/Components/Interfaces/TCPStreamService/TCPStreamService.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/EmbeddedServiceMethodBinderI.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/ExecutionInfo.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L1Portability/Threads.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/ThreadsB.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L1Portability/ExceptionHandler.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/ProcessorType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BitSet.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/ExecutionInfo.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/SingleThreadService.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/EmbeddedServiceI.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/EmbeddedServiceMethodBinderI.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/EmbeddedServiceMethodBinderT.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/EmbeddedThreadI.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/EmbeddedThread.h \
../../../..//Source/Components/Interfaces/UDPStream/UDPSTransportI.h \
../../../..//Source/Components/DataSources/UDPStreamer/UDPStreamer.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L1Portability/EventSem.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L5GAMs/MemoryDataSourceI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L5GAMs/DataSourceI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L5GAMs/StatefulI.h \
../../../..//Source/Components/Interfaces/UDPStream/UDPSServer.h \
/home/martino/workspace/MARTe2/Source/Core/FileSystem/L1Portability/BasicTCPSocket.h \
/home/martino/workspace/MARTe2/Source/Core/FileSystem/L1Portability/BasicSocket.h \
/home/martino/workspace/MARTe2/Source/Core/FileSystem/L1Portability/InternetHost.h \
/home/martino/workspace/MARTe2/Source/Core/FileSystem/L1Portability/Environment/Linux/InternetHostCore.h \
/home/martino/workspace/MARTe2/Source/Core/FileSystem/L1Portability/Environment/Linux/InternetMulticastCore.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/HandleI.h \
/home/martino/workspace/MARTe2/Source/Core/FileSystem/L1Portability/Environment/Linux/SocketCore.h \
/home/martino/workspace/MARTe2/Source/Core/FileSystem/L1Portability/BasicUDPSocket.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Sleep.h \
/home/martino/workspace/MARTe2/Source/Core/FileSystem/L1Portability/InternetHost.h
@@ -1,25 +0,0 @@
#############################################################
#
# 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
#
# 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 for the specific language governing
# permissions and limitations under the Licence.
#
#############################################################
include Makefile.inc
@@ -1,62 +0,0 @@
#############################################################
#
# 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
#
# 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 for the specific language governing
# permissions and limitations under the Licence.
#
#############################################################
OBJSX = TCPStreamerClient.x
PACKAGE=Components/DataSources
ROOT_DIR=../../../../
MAKEDEFAULTDIR=$(MARTe2_DIR)/MakeDefaults
include $(MAKEDEFAULTDIR)/MakeStdLibDefs.$(TARGET)
INCLUDES += -I.
INCLUDES += -I$(ROOT_DIR)/Common/UDP
INCLUDES += -I$(ROOT_DIR)/Source/Components/Interfaces/UDPStream
INCLUDES += -I$(ROOT_DIR)/Source/Components/DataSources/UDPStreamerClient
INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L0Types
INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L1Portability
INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L2Objects
INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L3Streams
INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L4Messages
INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L4Configuration
INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L5GAMs
INCLUDES += -I$(MARTe2_DIR)/Source/Core/Scheduler/L1Portability
INCLUDES += -I$(MARTe2_DIR)/Source/Core/Scheduler/L3Services
INCLUDES += -I$(MARTe2_DIR)/Source/Core/Scheduler/L4Messages
INCLUDES += -I$(MARTe2_DIR)/Source/Core/Scheduler/L5GAMs
INCLUDES += -I$(MARTe2_DIR)/Source/Core/FileSystem/L1Portability
INCLUDES += -I$(MARTe2_DIR)/Source/Core/FileSystem/L3Streams
LIBRARIES += -L$(MARTe2_DIR)/Build/$(TARGET)/Core -lMARTe2
LIBRARIES += -L$(ROOT_DIR)/Build/$(TARGET)/Components/Interfaces/UDPStream -lUDPStream
LIBRARIES += -L$(ROOT_DIR)/Build/$(TARGET)/Components/DataSources/UDPStreamerClient -lUDPStreamerClient
all: $(OBJS) \
$(BUILD_DIR)/TCPStreamerClient$(LIBEXT) \
$(BUILD_DIR)/TCPStreamerClient$(DLLEXT)
echo $(OBJS)
include depends.$(TARGET)
include $(MAKEDEFAULTDIR)/MakeStdLibRules.$(TARGET)
@@ -1,122 +0,0 @@
/**
* @file TCPStreamerClient.cpp
* @brief Source file for class TCPStreamerClient
* @date 05/11/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 source file contains the definition of all the methods for
* the class TCPStreamerClient (public, protected, and private).
*/
#define DLL_API
#include <string.h>
#include "AdvancedErrorManagement.h"
#include "ConfigurationDatabase.h"
#include "TCPStreamerClient.h"
namespace MARTe {
static const char8 *const tcpClientRejectedKeys[] = {
"MulticastGroup", "DataPort", "Interface", "KeepAliveInterval"
};
TCPStreamerClient::TCPStreamerClient() : UDPStreamerClient() {
tcpClient = new TCPSClient();
SetClient(tcpClient);
}
TCPStreamerClient::~TCPStreamerClient() {
/* tcpClient is owned (and deleted) by UDPStreamerClient. */
tcpClient = NULL_PTR(TCPSClient *);
}
bool TCPStreamerClient::Initialise(StructuredDataI &data) {
bool ok = true;
const uint32 numKeys = data.GetNumberOfChildren();
for (uint32 i = 0u; (i < numKeys) && ok; i++) {
const char8 *name = data.GetChildName(i);
if (name == NULL_PTR(const char8 *)) {
continue;
}
for (uint32 k = 0u; k < (sizeof(tcpClientRejectedKeys) / sizeof(tcpClientRejectedKeys[0])); k++) {
if (strcmp(name, tcpClientRejectedKeys[k]) == 0) {
REPORT_ERROR(ErrorManagement::ParametersError,
"Parameter '%s' is UDP-only and not supported by TCPStreamerClient.", name);
ok = false;
}
}
}
if (ok) {
ok = UDPStreamerClient::Initialise(data);
}
return ok;
}
bool TCPStreamerClient::IsConnected() const {
return tcpClient->IsConnected();
}
uint32 TCPStreamerClient::GetConnectionCount() const {
return tcpClient->GetConnectionCount();
}
uint32 TCPStreamerClient::GetProtocolErrors() const {
return tcpClient->GetProtocolErrors();
}
bool TCPStreamerClient::InitialiseTransport(StructuredDataI &data) {
/* The TCP defaults differ from the UDP ones (no silence timeout, 2 s
* reconnect delay), so read them here rather than from the base members. */
float32 silenceTimeout = 0.0F;
if (!data.Read("SilenceTimeout", silenceTimeout)) {
silenceTimeout = 0.0F;
}
float32 reconnectDelay = static_cast<float32>(TCPSClient::TCPS_CLIENT_DEFAULT_RECONNECT_DELAY_S);
if (!data.Read("ReconnectDelay", reconnectDelay)) {
reconnectDelay = static_cast<float32>(TCPSClient::TCPS_CLIENT_DEFAULT_RECONNECT_DELAY_S);
}
ConfigurationDatabase cdb;
bool ok = cdb.Write("ServerAddr", serverAddress);
if (ok) { ok = cdb.Write("Port", static_cast<uint32>(port)); }
if (ok) { ok = cdb.Write("SilenceTimeout", silenceTimeout); }
if (ok) { ok = cdb.Write("ReconnectDelay", reconnectDelay); }
if (ok) { ok = cdb.Write("CPUMask", cpuMask); }
if (ok) { ok = cdb.Write("StackSize", stackSize); }
StreamString streamName;
if (ok && data.Read("StreamName", streamName)) {
if (strchr(streamName.Buffer(), ',') != NULL_PTR(char8 *)) {
REPORT_ERROR(ErrorManagement::ParametersError,
"StreamName must be a single stream: a TCPStreamerClient cannot multiplex.");
ok = false;
}
if (ok) {
ok = cdb.Write("StreamName", streamName.Buffer());
}
}
if (ok) { ok = cdb.MoveToRoot(); }
if (ok) { ok = tcpClient->Initialise(cdb); }
if (!ok) {
REPORT_ERROR(ErrorManagement::ParametersError, "Could not configure the TCPSClient receiver.");
}
tcpClient->SetListener(this);
return ok;
}
CLASS_REGISTER(TCPStreamerClient, "1.0")
} /* namespace MARTe */
@@ -1,79 +0,0 @@
/**
* @file TCPStreamerClient.h
* @brief Header file for class TCPStreamerClient
* @date 05/11/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
* TCPStreamerClient with all of its public, protected and private members.
*/
#ifndef TCPSTREAMERCLIENT_H_
#define TCPSTREAMERCLIENT_H_
#include "TCPSClient.h"
#include "UDPStreamerClient.h"
namespace MARTe {
/**
* @brief A DataSource that receives a TCPStreamer (or any TCP UDPS server)
* stream and exposes it as input signals.
*
* @details Same signal handling and CONFIG validation as UDPStreamerClient; the
* transport is a TCPSClient that connects, reads framed UDPS packets, and
* reconnects automatically when the connection is lost.
*
* @par Configuration
* | Parameter | Type | Default | Description |
* |----------------|---------|-----------|-------------|
* | ServerAddress | string | 127.0.0.1 | Server IPv4 address or host name. |
* | Port | uint16 | 44500 | Server TCP port. |
* | SilenceTimeout | float32 | 0 | Seconds without any received byte before reconnecting; 0 disables. |
* | ReconnectDelay | float32 | 2 | Seconds between connection attempts. |
* | StreamName | string | *(absent)* | Stream to subscribe to on a TCPStreamService (omit for a plain TCPStreamer). |
* | CPUMask | uint32 | 0xFFFFFFFF| Receiver thread affinity. |
* | StackSize | uint32 | (default) | Receiver thread stack size. |
*
* MulticastGroup, DataPort, Interface and KeepAliveInterval are UDP-only and
* rejected.
*/
class TCPStreamerClient : public UDPStreamerClient {
public:
CLASS_REGISTER_DECLARATION()
TCPStreamerClient();
virtual ~TCPStreamerClient();
/** Rejects the UDP-only keys, then behaves as UDPStreamerClient::Initialise(). */
virtual bool Initialise(StructuredDataI &data);
/** @return true while connected to the server. */
bool IsConnected() const;
/** @return connections established so far. */
uint32 GetConnectionCount() const;
/** @return connections dropped because of a protocol violation. */
uint32 GetProtocolErrors() const;
protected:
virtual bool InitialiseTransport(StructuredDataI &data);
private:
TCPSClient *tcpClient; /**< Alias of the transport owned by UDPStreamerClient. */
};
} /* namespace MARTe */
#endif /* TCPSTREAMERCLIENT_H_ */
@@ -1,144 +0,0 @@
../../../..//Build/x86-linux/Components/DataSources/TCPStreamerClient/TCPStreamerClient.o: TCPStreamerClient.cpp \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/AdvancedErrorManagement.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ClassProperties.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/GeneralDefinitions.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/TypeCharacteristics.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/CompilerTypes.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/Architecture/x86_gcc/CompilerTypes.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/GeneralDefinitions.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/FractionalInteger.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/TypeCharacteristics.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/TemplateParametersVerificator.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/CompilerTypes.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/ErrorManagement.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/ErrorInformation.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/ErrorType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BitBoolean.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BitRange.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/GeneralDefinitions.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StreamI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/TimeoutType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/HighResolutionTimer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/TimeStamp.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Architecture/x86_gcc/HighResolutionTimerA.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/HighResolutionTimer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/../../HighResolutionTimer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/HighResolutionTimerCalibrator.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/../../GeneralDefinitions.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/TimeStamp.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/StreamMemoryReference.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/BufferedStreamI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/TimeoutType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/AnyType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ClassProperties.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ClassRegistryDatabase.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/GlobalObjectsDatabase.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/GlobalObjectI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/StandardHeap.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/../../HeapI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/../../ErrorManagement.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/../../GeneralDefinitions.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/../Generic/StandardHeap_Generic.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/FastPollingMutexSem.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Atomic.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Architecture/x86_gcc/AtomicA.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/ErrorManagement.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Sleep.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ClassRegistryItem.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/CString.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/ZeroTerminatedArray.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/Introspection.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/IntrospectionEntry.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/TypeDescriptor.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BasicType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/FractionalInteger.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BitRange.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BitBoolean.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/ZeroTerminatedArray.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/LinkedListable.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/LinkedListHolderT.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/IteratorT.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/Iterator.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/LinkedListable.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/LinkedListHolder.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/SortFilter.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/SearchFilter.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/LoadableLibrary.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ObjectBuilder.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/HeapI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StaticList.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StaticListHolder.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/Matrix.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/Vector.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/HeapManager.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/HeapI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/MemoryOperationsHelper.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/FormatDescriptor.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/IOBuffer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/CharBuffer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StreamI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L4Configuration/ConfigurationDatabase.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L4Configuration/AnyObject.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/Object.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StringHelper.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/StructuredDataI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/AnyType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/CLASSREGISTER.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ClassRegistryItemT.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ObjectBuilderT.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L4Configuration/ConfigurationDatabaseNode.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/Fnv1aHashFunction.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/HashFunction.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ReferenceContainer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/LinkedListHolder.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/Object.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/Reference.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ReferenceContainerFilter.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ReferenceContainerNode.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ReferenceT.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/UnorderedMap.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/HeapManager.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StringHelper.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ReferenceContainerFilterObjectName.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ReferenceContainer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/StructuredDataI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L4Configuration/TypeConversion.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/Vector.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/StreamString.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/StreamStringIOBuffer.h \
TCPStreamerClient.h \
../../../..//Source/Components/Interfaces/UDPStream/TCPSClient.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/EmbeddedServiceMethodBinderI.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/ExecutionInfo.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L1Portability/Threads.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/ThreadsB.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L1Portability/ExceptionHandler.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/ProcessorType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BitSet.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/ExecutionInfo.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/SingleThreadService.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/EmbeddedServiceI.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/EmbeddedServiceMethodBinderI.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/EmbeddedServiceMethodBinderT.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/EmbeddedThreadI.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/EmbeddedThread.h \
../../../..//Source/Components/Interfaces/UDPStream/UDPSClient.h \
/home/martino/workspace/MARTe2/Source/Core/FileSystem/L1Portability/BasicTCPSocket.h \
/home/martino/workspace/MARTe2/Source/Core/FileSystem/L1Portability/BasicSocket.h \
/home/martino/workspace/MARTe2/Source/Core/FileSystem/L1Portability/InternetHost.h \
/home/martino/workspace/MARTe2/Source/Core/FileSystem/L1Portability/Environment/Linux/InternetHostCore.h \
/home/martino/workspace/MARTe2/Source/Core/FileSystem/L1Portability/Environment/Linux/InternetMulticastCore.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/HandleI.h \
/home/martino/workspace/MARTe2/Source/Core/FileSystem/L1Portability/Environment/Linux/SocketCore.h \
/home/martino/workspace/MARTe2/Source/Core/FileSystem/L1Portability/BasicUDPSocket.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Sleep.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/HighResolutionTimer.h \
/home/martino/workspace/MARTe2/Source/Core/FileSystem/L1Portability/InternetHost.h \
../../../..//Common/UDP/UDPSProtocol.h \
../../../..//Source/Components/Interfaces/UDPStream/UDPSTransportI.h \
../../../..//Source/Components/DataSources/UDPStreamerClient/UDPStreamerClient.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L1Portability/EventSem.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L5GAMs/MemoryDataSourceI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L5GAMs/DataSourceI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L5GAMs/StatefulI.h \
../../../..//Source/Components/Interfaces/UDPStream/UDPSClient.h
@@ -1,144 +0,0 @@
TCPStreamerClient.o: TCPStreamerClient.cpp \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/AdvancedErrorManagement.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ClassProperties.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/GeneralDefinitions.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/TypeCharacteristics.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/CompilerTypes.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/Architecture/x86_gcc/CompilerTypes.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/GeneralDefinitions.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/FractionalInteger.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/TypeCharacteristics.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/TemplateParametersVerificator.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/CompilerTypes.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/ErrorManagement.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/ErrorInformation.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/ErrorType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BitBoolean.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BitRange.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/GeneralDefinitions.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StreamI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/TimeoutType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/HighResolutionTimer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/TimeStamp.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Architecture/x86_gcc/HighResolutionTimerA.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/HighResolutionTimer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/../../HighResolutionTimer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/HighResolutionTimerCalibrator.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/../../GeneralDefinitions.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/TimeStamp.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/StreamMemoryReference.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/BufferedStreamI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/TimeoutType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/AnyType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ClassProperties.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ClassRegistryDatabase.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/GlobalObjectsDatabase.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/GlobalObjectI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/StandardHeap.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/../../HeapI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/../../ErrorManagement.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/../../GeneralDefinitions.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/../Generic/StandardHeap_Generic.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/FastPollingMutexSem.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Atomic.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Architecture/x86_gcc/AtomicA.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/ErrorManagement.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Sleep.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ClassRegistryItem.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/CString.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/ZeroTerminatedArray.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/Introspection.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/IntrospectionEntry.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/TypeDescriptor.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BasicType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/FractionalInteger.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BitRange.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BitBoolean.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/ZeroTerminatedArray.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/LinkedListable.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/LinkedListHolderT.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/IteratorT.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/Iterator.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/LinkedListable.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/LinkedListHolder.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/SortFilter.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/SearchFilter.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/LoadableLibrary.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ObjectBuilder.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/HeapI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StaticList.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StaticListHolder.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/Matrix.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/Vector.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/HeapManager.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/HeapI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/MemoryOperationsHelper.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/FormatDescriptor.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/IOBuffer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/CharBuffer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StreamI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L4Configuration/ConfigurationDatabase.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L4Configuration/AnyObject.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/Object.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StringHelper.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/StructuredDataI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/AnyType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/CLASSREGISTER.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ClassRegistryItemT.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ObjectBuilderT.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L4Configuration/ConfigurationDatabaseNode.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/Fnv1aHashFunction.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/HashFunction.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ReferenceContainer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/LinkedListHolder.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/Object.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/Reference.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ReferenceContainerFilter.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ReferenceContainerNode.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ReferenceT.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/UnorderedMap.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/HeapManager.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StringHelper.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ReferenceContainerFilterObjectName.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ReferenceContainer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/StructuredDataI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L4Configuration/TypeConversion.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/Vector.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/StreamString.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/StreamStringIOBuffer.h \
TCPStreamerClient.h \
../../../..//Source/Components/Interfaces/UDPStream/TCPSClient.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/EmbeddedServiceMethodBinderI.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/ExecutionInfo.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L1Portability/Threads.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/ThreadsB.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L1Portability/ExceptionHandler.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/ProcessorType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BitSet.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/ExecutionInfo.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/SingleThreadService.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/EmbeddedServiceI.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/EmbeddedServiceMethodBinderI.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/EmbeddedServiceMethodBinderT.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/EmbeddedThreadI.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/EmbeddedThread.h \
../../../..//Source/Components/Interfaces/UDPStream/UDPSClient.h \
/home/martino/workspace/MARTe2/Source/Core/FileSystem/L1Portability/BasicTCPSocket.h \
/home/martino/workspace/MARTe2/Source/Core/FileSystem/L1Portability/BasicSocket.h \
/home/martino/workspace/MARTe2/Source/Core/FileSystem/L1Portability/InternetHost.h \
/home/martino/workspace/MARTe2/Source/Core/FileSystem/L1Portability/Environment/Linux/InternetHostCore.h \
/home/martino/workspace/MARTe2/Source/Core/FileSystem/L1Portability/Environment/Linux/InternetMulticastCore.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/HandleI.h \
/home/martino/workspace/MARTe2/Source/Core/FileSystem/L1Portability/Environment/Linux/SocketCore.h \
/home/martino/workspace/MARTe2/Source/Core/FileSystem/L1Portability/BasicUDPSocket.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Sleep.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/HighResolutionTimer.h \
/home/martino/workspace/MARTe2/Source/Core/FileSystem/L1Portability/InternetHost.h \
../../../..//Common/UDP/UDPSProtocol.h \
../../../..//Source/Components/Interfaces/UDPStream/UDPSTransportI.h \
../../../..//Source/Components/DataSources/UDPStreamerClient/UDPStreamerClient.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L1Portability/EventSem.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L5GAMs/MemoryDataSourceI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L5GAMs/DataSourceI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L5GAMs/StatefulI.h \
../../../..//Source/Components/Interfaces/UDPStream/UDPSClient.h
@@ -84,7 +84,6 @@ UDPStreamer::UDPStreamer()
: MemoryDataSourceI(), EmbeddedServiceMethodBinderI(), executor(*this) { : MemoryDataSourceI(), EmbeddedServiceMethodBinderI(), executor(*this) {
port = UDPS_DEFAULT_PORT; port = UDPS_DEFAULT_PORT;
maxPayloadSize = UDPS_DEFAULT_MAX_PAYLOAD; maxPayloadSize = UDPS_DEFAULT_MAX_PAYLOAD;
server = new UDPSServer();
cpuMask = 0xFFFFFFFFu; cpuMask = 0xFFFFFFFFu;
stackSize = THREADS_DEFAULT_STACKSIZE; stackSize = THREADS_DEFAULT_STACKSIZE;
publishMode = UDPStreamerPublishStrict; publishMode = UDPStreamerPublishStrict;
@@ -108,9 +107,6 @@ UDPStreamer::UDPStreamer()
readyTimestamps = NULL_PTR(uint64 *); readyTimestamps = NULL_PTR(uint64 *);
scratchTimestamps = NULL_PTR(uint64 *); scratchTimestamps = NULL_PTR(uint64 *);
readyFill = 0u; readyFill = 0u;
readySnapshotPending = false;
droppedPublications = 0u;
lastDropReportTicks = 0u;
decimateRatio = 1u; decimateRatio = 1u;
decimateCounter = 0u; decimateCounter = 0u;
@@ -136,11 +132,7 @@ UDPStreamer::~UDPStreamer() {
} }
} }
if (server != NULL_PTR(UDPSServerI *)) { (void)server.Stop();
(void)server->Stop();
delete server;
server = NULL_PTR(UDPSServerI *);
}
HeapI *heapAccum = GlobalObjectsDatabase::Instance()->GetStandardHeap(); HeapI *heapAccum = GlobalObjectsDatabase::Instance()->GetStandardHeap();
if (accumBuffer != NULL_PTR(uint8 *)) { if (accumBuffer != NULL_PTR(uint8 *)) {
@@ -197,7 +189,7 @@ bool UDPStreamer::Initialise(StructuredDataI &data) {
if (ok) { if (ok) {
if (!data.Read("MaxPayloadSize", maxPayloadSize)) { if (!data.Read("MaxPayloadSize", maxPayloadSize)) {
maxPayloadSize = DefaultMaxPayloadSize(); maxPayloadSize = UDPS_DEFAULT_MAX_PAYLOAD;
REPORT_ERROR(ErrorManagement::Information, REPORT_ERROR(ErrorManagement::Information,
"MaxPayloadSize not specified; using default %u.", "MaxPayloadSize not specified; using default %u.",
maxPayloadSize); maxPayloadSize);
@@ -291,58 +283,38 @@ bool UDPStreamer::Initialise(StructuredDataI &data) {
} }
if (ok) { if (ok) {
ok = InitialiseTransport(data); // Build server config with already-resolved Port and MaxPayloadSize so
// UDPSServer::Initialise() always sees them, even when defaults were used.
ConfigurationDatabase serverCfg;
(void)serverCfg.Write("Port", static_cast<uint32>(port));
(void)serverCfg.Write("MaxPayloadSize", maxPayloadSize);
// Forward optional multicast / timeout params if present in caller's data.
StreamString mcGroup;
if (data.Read("MulticastGroup", mcGroup) && (mcGroup.Size() > 0u)) {
(void)serverCfg.Write("MulticastGroup", mcGroup.Buffer());
uint32 dp = 0u;
if (data.Read("DataPort", dp)) {
(void)serverCfg.Write("DataPort", dp);
}
StreamString iface;
if (data.Read("Interface", iface)) {
(void)serverCfg.Write("Interface", iface);
} else {
ok = false;
REPORT_ERROR(ErrorManagement::InitialisationError,
"Missing mandatory interface for multicasting");
}
}
uint32 clientTimeout = 0u;
if (data.Read("ClientTimeout", clientTimeout)) {
(void)serverCfg.Write("ClientTimeout", clientTimeout);
}
ok = server.Initialise(serverCfg);
} }
return ok; return ok;
} }
uint32 UDPStreamer::DefaultMaxPayloadSize() const {
return UDPS_DEFAULT_MAX_PAYLOAD;
}
bool UDPStreamer::InitialiseTransport(StructuredDataI &data) {
bool ok = true;
// Build server config with already-resolved Port and MaxPayloadSize so
// UDPSServer::Initialise() always sees them, even when defaults were used.
ConfigurationDatabase serverCfg;
(void)serverCfg.Write("Port", static_cast<uint32>(port));
(void)serverCfg.Write("MaxPayloadSize", maxPayloadSize);
// Forward optional multicast / timeout params if present in caller's data.
StreamString mcGroup;
if (data.Read("MulticastGroup", mcGroup) && (mcGroup.Size() > 0u)) {
(void)serverCfg.Write("MulticastGroup", mcGroup.Buffer());
uint32 dp = 0u;
if (data.Read("DataPort", dp)) {
(void)serverCfg.Write("DataPort", dp);
}
StreamString iface;
if (data.Read("Interface", iface)) {
(void)serverCfg.Write("Interface", iface);
} else {
ok = false;
REPORT_ERROR(ErrorManagement::InitialisationError,
"Missing mandatory interface for multicasting");
}
}
uint32 clientTimeout = 0u;
if (data.Read("ClientTimeout", clientTimeout)) {
(void)serverCfg.Write("ClientTimeout", clientTimeout);
}
ok = server->Initialise(serverCfg);
return ok;
}
void UDPStreamer::SetServer(UDPSServerI *newServer) {
if (server != NULL_PTR(UDPSServerI *)) {
delete server;
}
server = newServer;
}
UDPSServerI *UDPStreamer::Transport() const { return server; }
bool UDPStreamer::SetConfiguredDatabase(StructuredDataI &data) { bool UDPStreamer::SetConfiguredDatabase(StructuredDataI &data) {
bool ok = MemoryDataSourceI::SetConfiguredDatabase(data); bool ok = MemoryDataSourceI::SetConfiguredDatabase(data);
if (!ok) { if (!ok) {
@@ -829,21 +801,19 @@ bool UDPStreamer::PrepareNextState(const char8 *const currentStateName,
const char8 *const nextStateName) { const char8 *const nextStateName) {
bool ok = true; bool ok = true;
ok = server->Start(); ok = server.Start();
/* Build the CONFIG payload and cache it in the server so any CONNECT client /* Build the CONFIG payload and cache it in the server so any CONNECT client
* receives it immediately. The config is static for the lifetime of this * receives it immediately. The config is static for the lifetime of this
* state. */ * state. */
if (ok) { if (ok) {
/* numSigs + descriptors + publishMode + hrtFrequency (+ slack). */ uint32 configBufSize = 4u + (numSigs * UDPS_SIGNAL_DESC_SIZE) + 32u + 1u;
uint32 configBufSize =
4u + (numSigs * UDPS_SIGNAL_DESC_SIZE) + 1u + 8u + 32u;
HeapI *heap = GlobalObjectsDatabase::Instance()->GetStandardHeap(); HeapI *heap = GlobalObjectsDatabase::Instance()->GetStandardHeap();
uint8 *cfgBuf = reinterpret_cast<uint8 *>(heap->Malloc(configBufSize)); uint8 *cfgBuf = reinterpret_cast<uint8 *>(heap->Malloc(configBufSize));
if (cfgBuf != NULL_PTR(uint8 *)) { if (cfgBuf != NULL_PTR(uint8 *)) {
uint32 cfgPayloadSize = 0u; uint32 cfgPayloadSize = 0u;
if (BuildConfigPayload(cfgBuf, configBufSize, cfgPayloadSize)) { if (BuildConfigPayload(cfgBuf, configBufSize, cfgPayloadSize)) {
(void)server->SendConfig(cfgBuf, cfgPayloadSize); (void)server.SendConfig(cfgBuf, cfgPayloadSize);
} else { } else {
REPORT_ERROR(ErrorManagement::Warning, REPORT_ERROR(ErrorManagement::Warning,
"Could not build initial CONFIG payload."); "Could not build initial CONFIG payload.");
@@ -901,11 +871,6 @@ bool UDPStreamer::Synchronise() {
/* HI-3: if accumFill reached maxBatchCount, force-flush before writing */ /* HI-3: if accumFill reached maxBatchCount, force-flush before writing */
if (accumFill >= maxBatchCount) { if (accumFill >= maxBatchCount) {
uint32 filled = accumFill; uint32 filled = accumFill;
if (readyFill > 0u) {
/* The sender has not taken the previous batch: it is about to be
* overwritten and its cycles will never reach any receiver. */
droppedPublications++;
}
(void)MemoryOperationsHelper::Copy(readyBuffer, accumBuffer, (void)MemoryOperationsHelper::Copy(readyBuffer, accumBuffer,
filled * totalSrcBytes); filled * totalSrcBytes);
(void)MemoryOperationsHelper::Copy( (void)MemoryOperationsHelper::Copy(
@@ -936,9 +901,6 @@ bool UDPStreamer::Synchronise() {
if (sizeCondition || timeCondition) { if (sizeCondition || timeCondition) {
bufMutex.FastLock(TTInfiniteWait); bufMutex.FastLock(TTInfiniteWait);
if (readyFill > 0u) {
droppedPublications++;
}
(void)MemoryOperationsHelper::Copy(readyBuffer, accumBuffer, (void)MemoryOperationsHelper::Copy(readyBuffer, accumBuffer,
filled * totalSrcBytes); filled * totalSrcBytes);
(void)MemoryOperationsHelper::Copy( (void)MemoryOperationsHelper::Copy(
@@ -960,24 +922,16 @@ bool UDPStreamer::Synchronise() {
if (decimateCounter >= decimateRatio) { if (decimateCounter >= decimateRatio) {
decimateCounter = 0u; decimateCounter = 0u;
bufMutex.FastLock(TTInfiniteWait); bufMutex.FastLock(TTInfiniteWait);
if (readySnapshotPending) {
droppedPublications++;
}
(void)MemoryOperationsHelper::Copy(readyBuffer, memory, totalSrcBytes); (void)MemoryOperationsHelper::Copy(readyBuffer, memory, totalSrcBytes);
syncTimestamp = ts; syncTimestamp = ts;
readySnapshotPending = true;
bufMutex.FastUnLock(); bufMutex.FastUnLock();
(void)dataSem.Post(); (void)dataSem.Post();
} }
} else { } else {
/* --- Strict path: post every call --- */ /* --- Strict path: post every call --- */
bufMutex.FastLock(TTInfiniteWait); bufMutex.FastLock(TTInfiniteWait);
if (readySnapshotPending) {
droppedPublications++;
}
(void)MemoryOperationsHelper::Copy(readyBuffer, memory, totalSrcBytes); (void)MemoryOperationsHelper::Copy(readyBuffer, memory, totalSrcBytes);
syncTimestamp = ts; syncTimestamp = ts;
readySnapshotPending = true;
bufMutex.FastUnLock(); bufMutex.FastUnLock();
(void)dataSem.Post(); (void)dataSem.Post();
} }
@@ -1001,87 +955,74 @@ ErrorManagement::ErrorType UDPStreamer::Execute(ExecutionInfo &info) {
} }
if (info.GetStage() == ExecutionInfo::MainStage) { if (info.GetStage() == ExecutionInfo::MainStage) {
/* --- Wait for the RT thread to publish new data --- /* --- Wait for RT thread to post new data ---
* dataSem is only a wake-up hint, never the record of pending work: * ResetWait sleeps the background thread until the RT thread calls
* EventSem::ResetWait resets the semaphore before waiting, so a Post that * Synchronise() and posts dataSem, or until the timeout expires.
* landed while this thread was inside ServiceClients()/SendData() is * Doing this FIRST means the thread spends nearly all its time here
* destroyed by the next Reset. Deciding what to send from the wait result * instead of spinning on the non-blocking select() below.
* would then skip that publication entirely, and the next flush would * Command latency is bounded by UDPS_DATA_WAIT_MS (acceptable for
* overwrite it — the receiver sees the batch's whole time span missing. * CONNECT / DISCONNECT). */
* The buffers therefore carry the state, and are only waited on when they ErrorManagement::ErrorType waitErr =
* are empty (which also avoids paying the wait when work is already dataSem.ResetWait(TimeoutType(UDPS_DATA_WAIT_MS));
* queued). */ bool dataReady = (waitErr == ErrorManagement::NoError);
if (!HasPendingPublication()) {
(void)dataSem.ResetWait(TimeoutType(UDPS_DATA_WAIT_MS));
}
/* --- Poll for incoming control commands (CONNECT / DISCONNECT / ACK) --- /* --- Poll for incoming control commands (CONNECT / DISCONNECT / ACK) ---
*/ */
server->ServiceClients(); server.ServiceClients();
/* Synchronise() already gates publication to the correct rate (size/time if (dataReady && server.HasClients()) {
* for Accumulate, every-Nth for Decimate, every call for Strict). The /* Synchronise() already gates posting dataSem to the correct rate
* pending publication is consumed whether or not anyone is listening, so * (size/time for Accumulate, every-Nth for Decimate, every call for
* that a client-less streamer neither spins here nor delivers a stale * Strict). Execute() just sends whatever is in the ready buffers. */
* snapshot to the next client that connects. */ if (publishMode == UDPStreamerPublishAccumulate) {
if (publishMode == UDPStreamerPublishAccumulate) { /* --- Accumulate batch send --- */
/* --- Accumulate batch send --- */ uint32 fill = 0u;
uint32 fill = 0u; bufMutex.FastLock(TTInfiniteWait);
bufMutex.FastLock(TTInfiniteWait); fill = readyFill;
fill = readyFill; if (fill > 0u) {
if (fill > 0u) { (void)MemoryOperationsHelper::Copy(scratchBuffer, readyBuffer,
(void)MemoryOperationsHelper::Copy(scratchBuffer, readyBuffer, fill * totalSrcBytes);
fill * totalSrcBytes); (void)MemoryOperationsHelper::Copy(
(void)MemoryOperationsHelper::Copy( reinterpret_cast<uint8 *>(scratchTimestamps),
reinterpret_cast<uint8 *>(scratchTimestamps), reinterpret_cast<const uint8 *>(readyTimestamps),
reinterpret_cast<const uint8 *>(readyTimestamps), fill * static_cast<uint32>(sizeof(uint64)));
fill * static_cast<uint32>(sizeof(uint64)));
readyFill = 0u;
}
bufMutex.FastUnLock();
if ((fill > 0u) && server->HasClients()) {
SerializeAccumulated(scratchBuffer, scratchTimestamps, fill);
uint32 sendBytes =
UDPS_TIMESTAMP_BYTES + 4u + fill * singleCycleWireBytes;
packetCounter++;
if (!server->SendData(packetCounter, wireBuffer, sendBytes)) {
REPORT_ERROR(ErrorManagement::Warning,
"Failed to send Accumulate DATA packet (counter=%u).",
packetCounter);
} }
} bufMutex.FastUnLock();
} else {
/* --- Single-snapshot send (Strict or Decimate) --- */ if (fill > 0u) {
uint64 ts = 0u; SerializeAccumulated(scratchBuffer, scratchTimestamps, fill);
bool pending = false; uint32 sendBytes =
bufMutex.FastLock(TTInfiniteWait); UDPS_TIMESTAMP_BYTES + 4u + fill * singleCycleWireBytes;
pending = readySnapshotPending; packetCounter++;
if (pending) { if (!server.SendData(packetCounter, wireBuffer, sendBytes)) {
REPORT_ERROR(ErrorManagement::Warning,
"Failed to send Accumulate DATA packet (counter=%u).",
packetCounter);
}
}
} else {
/* --- Single-snapshot send (Strict or Decimate) --- */
uint64 ts = 0u;
bufMutex.FastLock(TTInfiniteWait);
(void)MemoryOperationsHelper::Copy(scratchBuffer, readyBuffer, (void)MemoryOperationsHelper::Copy(scratchBuffer, readyBuffer,
totalSrcBytes); totalSrcBytes);
ts = syncTimestamp; ts = syncTimestamp;
readySnapshotPending = false; bufMutex.FastUnLock();
}
bufMutex.FastUnLock();
if (pending && server->HasClients()) {
QuantizeAndSerialize(scratchBuffer, ts); QuantizeAndSerialize(scratchBuffer, ts);
packetCounter++; packetCounter++;
if (!server->SendData(packetCounter, wireBuffer, totalWireBytes)) { if (!server.SendData(packetCounter, wireBuffer, totalWireBytes)) {
REPORT_ERROR(ErrorManagement::Warning, REPORT_ERROR(ErrorManagement::Warning,
"Failed to send DATA packet (counter=%u).", "Failed to send DATA packet (counter=%u).",
packetCounter); packetCounter);
} }
} }
} }
ReportDroppedPublications();
} }
if (info.GetStage() == ExecutionInfo::TerminationStage) { if (info.GetStage() == ExecutionInfo::TerminationStage) {
(void)server->Stop(); (void)server.Stop();
REPORT_ERROR(ErrorManagement::Information, REPORT_ERROR(ErrorManagement::Information,
"UDPStreamer background thread terminated."); "UDPStreamer background thread terminated.");
} }
@@ -1271,16 +1212,6 @@ bool UDPStreamer::BuildConfigPayload(uint8 *buf, uint32 bufSize,
buf[payloadSize] = static_cast<uint8>(publishMode); buf[payloadSize] = static_cast<uint8>(publishMode);
payloadSize += 1u; payloadSize += 1u;
/* 8 bytes: this host's HRT tick rate. DATA packets carry raw counter
* values, so a receiver on another machine cannot turn them into seconds
* without it. */
if ((payloadSize + 8u) > bufSize) {
return false;
}
uint64 hrtFrequency = HighResolutionTimer::Frequency();
(void)MemoryOperationsHelper::Copy(buf + payloadSize, &hrtFrequency, 8u);
payloadSize += 8u;
return true; return true;
} }
@@ -1392,39 +1323,9 @@ uint16 UDPStreamer::GetPort() const { return port; }
uint32 UDPStreamer::GetMaxPayloadSize() const { return maxPayloadSize; } uint32 UDPStreamer::GetMaxPayloadSize() const { return maxPayloadSize; }
bool UDPStreamer::IsClientConnected() const { return server->HasClients(); } bool UDPStreamer::IsClientConnected() const { return server.HasClients(); }
bool UDPStreamer::IsMulticast() const { return server->IsMulticast(); } bool UDPStreamer::IsMulticast() const { return server.IsMulticast(); }
uint32 UDPStreamer::GetDroppedPublications() const { return droppedPublications; }
bool UDPStreamer::HasPendingPublication() {
bool pending = false;
bufMutex.FastLock(TTInfiniteWait);
pending = (readyFill > 0u) || readySnapshotPending;
bufMutex.FastUnLock();
return pending;
}
void UDPStreamer::ReportDroppedPublications() {
uint64 now = HighResolutionTimer::Counter();
if ((now - lastDropReportTicks) < HighResolutionTimer::Frequency()) {
return;
}
lastDropReportTicks = now;
uint32 dropped = 0u;
bufMutex.FastLock(TTInfiniteWait);
dropped = droppedPublications;
bufMutex.FastUnLock();
if (dropped > 0u) {
REPORT_ERROR(ErrorManagement::Warning,
"Dropped %u unsent publication(s) so far: the sender thread is "
"not keeping up with the RT cycle.",
dropped);
}
}
CLASS_REGISTER(UDPStreamer, "1.0") CLASS_REGISTER(UDPStreamer, "1.0")
@@ -39,7 +39,6 @@
#include "SingleThreadService.h" #include "SingleThreadService.h"
#include "StreamString.h" #include "StreamString.h"
#include "UDPSServer.h" #include "UDPSServer.h"
#include "UDPSTransportI.h"
/*---------------------------------------------------------------------------*/ /*---------------------------------------------------------------------------*/
/* Class declaration */ /* Class declaration */
@@ -323,44 +322,6 @@ public:
*/ */
bool IsMulticast() const; 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;
protected:
/**
* @brief MaxPayloadSize used when the configuration does not specify one.
* @details Transport specific (a datagram budget for UDP, a frame budget for
* TCP), hence overridable. Called from Initialise(), not the constructor.
*/
virtual uint32 DefaultMaxPayloadSize() const;
/**
* @brief Configures the transport from the already-resolved Port and
* MaxPayloadSize plus whatever transport keys @p data carries.
* @details The default implementation configures the UDPSServer (unicast /
* multicast). A subclass that installs another transport with SetServer()
* overrides this to configure it instead.
*/
virtual bool InitialiseTransport(StructuredDataI &data);
/**
* @brief Replaces the transport. Takes ownership; only valid before Start().
*/
void SetServer(UDPSServerI *newServer);
/**
* @brief The transport currently in use (owned by this object).
*/
UDPSServerI *Transport() const;
private: private:
/** /**
* @brief Serializes the CONFIG payload into buf and sets payloadSize. * @brief Serializes the CONFIG payload into buf and sets payloadSize.
@@ -388,18 +349,6 @@ private:
*/ */
static uint8 TypeDescriptorToCode(TypeDescriptor td); 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 */ /* Configuration parameters */
uint16 port; /**< UDP server port */ uint16 port; /**< UDP server port */
uint32 maxPayloadSize; /**< Max payload bytes per UDP packet (excluding header) */ uint32 maxPayloadSize; /**< Max payload bytes per UDP packet (excluding header) */
@@ -418,13 +367,6 @@ private:
uint64 *readyTimestamps; /**< Heap: [maxBatchCount] HRT for completed ready batch */ uint64 *readyTimestamps; /**< Heap: [maxBatchCount] HRT for completed ready batch */
uint64 *scratchTimestamps; /**< Heap: [maxBatchCount] background-thread local copy */ uint64 *scratchTimestamps; /**< Heap: [maxBatchCount] background-thread local copy */
uint32 readyFill; /**< Snapshot count in the ready batch */ 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 */ /* Decimate mode */
uint32 decimateRatio; /**< Send 1 packet every decimateRatio Synchronise() calls */ uint32 decimateRatio; /**< Send 1 packet every decimateRatio Synchronise() calls */
uint32 decimateCounter; /**< Current decimate cycle counter */ uint32 decimateCounter; /**< Current decimate cycle counter */
@@ -445,8 +387,8 @@ private:
uint8 *wireBuffer; /**< Pre-allocated buffer for the serialized DATA payload */ uint8 *wireBuffer; /**< Pre-allocated buffer for the serialized DATA payload */
uint32 totalWireBytes; /**< Total bytes of all signals after quantization + 8-byte timestamp prefix */ uint32 totalWireBytes; /**< Total bytes of all signals after quantization + 8-byte timestamp prefix */
/* Networking — handled by a UDPSServerI (UDPSServer by default) */ /* Networking — handled by UDPSServer */
UDPSServerI *server; /**< Owned transport; see SetServer() */ UDPSServer server; /**< Multi-client session manager (unicast and multicast) */
/* Thread management */ /* Thread management */
SingleThreadService executor; /**< Background thread service */ SingleThreadService executor; /**< Background thread service */
@@ -135,6 +135,4 @@
/home/martino/workspace/MARTe2/Source/Core/FileSystem/L1Portability/BasicUDPSocket.h \ /home/martino/workspace/MARTe2/Source/Core/FileSystem/L1Portability/BasicUDPSocket.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Sleep.h \ /home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Sleep.h \
/home/martino/workspace/MARTe2/Source/Core/FileSystem/L1Portability/InternetHost.h \ /home/martino/workspace/MARTe2/Source/Core/FileSystem/L1Portability/InternetHost.h \
../../../..//Common/UDP/UDPSProtocol.h \ ../../../..//Common/UDP/UDPSProtocol.h
../../../..//Source/Components/Interfaces/UDPStream/UDPSTransportI.h \
../../../..//Source/Components/Interfaces/UDPStream/UDPSTransportI.h
@@ -135,6 +135,4 @@ UDPStreamer.o: UDPStreamer.cpp \
/home/martino/workspace/MARTe2/Source/Core/FileSystem/L1Portability/BasicUDPSocket.h \ /home/martino/workspace/MARTe2/Source/Core/FileSystem/L1Portability/BasicUDPSocket.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Sleep.h \ /home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Sleep.h \
/home/martino/workspace/MARTe2/Source/Core/FileSystem/L1Portability/InternetHost.h \ /home/martino/workspace/MARTe2/Source/Core/FileSystem/L1Portability/InternetHost.h \
../../../..//Common/UDP/UDPSProtocol.h \ ../../../..//Common/UDP/UDPSProtocol.h
../../../..//Source/Components/Interfaces/UDPStream/UDPSTransportI.h \
../../../..//Source/Components/Interfaces/UDPStream/UDPSTransportI.h
@@ -131,8 +131,7 @@ static float64 DequantValue(float64 raw, uint8 quantType,
UDPStreamerClient::UDPStreamerClient() : UDPStreamerClient::UDPStreamerClient() :
MemoryDataSourceI(), MemoryDataSourceI(),
UDPSClientListener(), UDPSClientListener(),
client(NULL_PTR(UDPSClientI *)) { client() {
client = new UDPSClient();
serverAddress = UDPS_CLIENT_DEFAULT_ADDR; serverAddress = UDPS_CLIENT_DEFAULT_ADDR;
port = UDPS_CLIENT_DEFAULT_PORT; port = UDPS_CLIENT_DEFAULT_PORT;
maxPayloadSize = UDPS_CLIENT_DEFAULT_MAX_PAYLOAD; maxPayloadSize = UDPS_CLIENT_DEFAULT_MAX_PAYLOAD;
@@ -161,13 +160,9 @@ UDPStreamerClient::UDPStreamerClient() :
UDPStreamerClient::~UDPStreamerClient() { UDPStreamerClient::~UDPStreamerClient() {
(void) dataSem.Post(); (void) dataSem.Post();
if (client != NULL_PTR(UDPSClientI *)) { if (receiverUp) {
if (receiverUp) { (void) client.Stop();
(void) client->Stop(); receiverUp = false;
receiverUp = false;
}
delete client;
client = NULL_PTR(UDPSClientI *);
} }
HeapI *heap = GlobalObjectsDatabase::Instance()->GetStandardHeap(); HeapI *heap = GlobalObjectsDatabase::Instance()->GetStandardHeap();
@@ -263,47 +258,34 @@ bool UDPStreamerClient::Initialise(StructuredDataI &data) {
} }
} }
if (ok) {
ok = InitialiseTransport(data);
}
return ok;
}
bool UDPStreamerClient::InitialiseTransport(StructuredDataI &data) {
/* Forward the transport parameters to the shared UDPSClient receiver, /* Forward the transport parameters to the shared UDPSClient receiver,
* translating the DataSource parameter names to the UDPSClient keys. */ * translating the DataSource parameter names to the UDPSClient keys. */
bool ok = true; if (ok) {
ConfigurationDatabase cdb; ConfigurationDatabase cdb;
ok = cdb.Write("ServerAddr", serverAddress); ok = cdb.Write("ServerAddr", serverAddress);
if (ok) { ok = cdb.Write("Port", static_cast<uint32>(port)); } if (ok) { ok = cdb.Write("Port", static_cast<uint32>(port)); }
if (ok && useMulticast) { if (ok && useMulticast) {
ok = cdb.Write("MulticastGroup", multicastGroup); ok = cdb.Write("MulticastGroup", multicastGroup);
if (ok) { ok = cdb.Write("DataPort", static_cast<uint32>(dataPort)); } if (ok) { ok = cdb.Write("DataPort", static_cast<uint32>(dataPort)); }
if (ok && (multicastInterface.Size() > 0u)) { if (ok && (multicastInterface.Size() > 0u)) {
ok = cdb.Write("Interface", multicastInterface); ok = cdb.Write("Interface", multicastInterface);
}
} }
if (ok) { ok = cdb.Write("MaxPayloadSize", maxPayloadSize); }
if (ok) { ok = cdb.Write("KeepAliveInterval", keepAliveInterval); }
if (ok) { ok = cdb.Write("SilenceTimeout", silenceTimeout); }
if (ok) { ok = cdb.Write("CPUMask", cpuMask); }
if (ok) { ok = cdb.Write("StackSize", stackSize); }
if (ok) { ok = cdb.MoveToRoot(); }
if (ok) { ok = client.Initialise(cdb); }
if (!ok) {
REPORT_ERROR(ErrorManagement::ParametersError,
"Could not configure the UDPSClient receiver.");
}
client.SetListener(this);
} }
if (ok) { ok = cdb.Write("MaxPayloadSize", maxPayloadSize); }
if (ok) { ok = cdb.Write("KeepAliveInterval", keepAliveInterval); }
if (ok) { ok = cdb.Write("SilenceTimeout", silenceTimeout); }
if (ok) { ok = cdb.Write("CPUMask", cpuMask); }
if (ok) { ok = cdb.Write("StackSize", stackSize); }
if (ok) { ok = cdb.MoveToRoot(); }
if (ok) { ok = client->Initialise(cdb); }
if (!ok) {
REPORT_ERROR(ErrorManagement::ParametersError,
"Could not configure the UDPSClient receiver.");
}
client->SetListener(this);
return ok;
}
void UDPStreamerClient::SetClient(UDPSClientI *newClient) { return ok;
if (client != NULL_PTR(UDPSClientI *)) {
delete client;
}
client = newClient;
} }
bool UDPStreamerClient::SetConfiguredDatabase(StructuredDataI &data) { bool UDPStreamerClient::SetConfiguredDatabase(StructuredDataI &data) {
@@ -395,7 +377,7 @@ bool UDPStreamerClient::PrepareNextState(const char8 *const currentStateName,
const char8 *const nextStateName) { const char8 *const nextStateName) {
bool ok = true; bool ok = true;
if (!receiverUp) { if (!receiverUp) {
ok = client->Start(); ok = client.Start();
if (ok) { if (ok) {
receiverUp = true; receiverUp = true;
} }
@@ -160,27 +160,6 @@ public:
*/ */
virtual void OnUDPSDisconnected(); virtual void OnUDPSDisconnected();
protected:
/**
* @brief Configures the transport from the already-parsed members and
* registers this object as its listener.
* @details The default implementation configures the UDPSClient (unicast /
* multicast). A subclass that installs another transport with SetClient()
* overrides this to configure it instead.
*/
virtual bool InitialiseTransport(StructuredDataI &data);
/**
* @brief Replaces the transport. Takes ownership; only valid before Start().
*/
void SetClient(UDPSClientI *newClient);
/* Configuration parameters shared with transport subclasses */
StreamString serverAddress; /**< Server IP address. */
uint16 port; /**< Server port. */
uint32 cpuMask; /**< Background thread CPU affinity. */
uint32 stackSize; /**< Background thread stack size. */
private: private:
/** /**
* @brief Decodes a single snapshot from a DATA payload into @p dst. * @brief Decodes a single snapshot from a DATA payload into @p dst.
@@ -192,9 +171,13 @@ private:
void DecodeSnapshot(const uint8 *payload, uint32 size, uint8 *dst); void DecodeSnapshot(const uint8 *payload, uint32 size, uint8 *dst);
/* Configuration parameters */ /* Configuration parameters */
StreamString serverAddress; /**< Server IP address. */
uint16 port; /**< Server port. */
uint32 maxPayloadSize; /**< Max payload bytes per datagram. */ uint32 maxPayloadSize; /**< Max payload bytes per datagram. */
uint32 keepAliveInterval; /**< Seconds between unicast keepalive ACKs (0 disables). */ uint32 keepAliveInterval; /**< Seconds between unicast keepalive ACKs (0 disables). */
float32 silenceTimeout; /**< Seconds of no data before reconnect (sub-second allowed, 0 disables). */ float32 silenceTimeout; /**< Seconds of no data before reconnect (sub-second allowed, 0 disables). */
uint32 cpuMask; /**< Background thread CPU affinity. */
uint32 stackSize; /**< Background thread stack size. */
StreamString multicastGroup; /**< Multicast group IP; empty = unicast. */ StreamString multicastGroup; /**< Multicast group IP; empty = unicast. */
StreamString multicastInterface; /**< Local IPv4 address for multicast join; empty = INADDR_ANY. */ StreamString multicastInterface; /**< Local IPv4 address for multicast join; empty = INADDR_ANY. */
uint16 dataPort; /**< UDP port for DATA datagrams (multicast). */ uint16 dataPort; /**< UDP port for DATA datagrams (multicast). */
@@ -208,7 +191,7 @@ private:
uint8 publishMode; /**< UDPS_PUBLISH_* from CONFIG. */ uint8 publishMode; /**< UDPS_PUBLISH_* from CONFIG. */
/* Shared receiver (same code base as the StreamHub hub) */ /* Shared receiver (same code base as the StreamHub hub) */
UDPSClientI *client; /**< Owned transport (UDPSClient by default); see SetClient(). */ UDPSClient client; /**< Transport, reassembly and reconnect. */
bool receiverUp; /**< True once the receiver thread is started. */ bool receiverUp; /**< True once the receiver thread is started. */
/* Double-buffering for RT safety */ /* Double-buffering for RT safety */
@@ -136,5 +136,4 @@
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/EmbeddedServiceMethodBinderI.h \ /home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/EmbeddedServiceMethodBinderI.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/EmbeddedServiceMethodBinderT.h \ /home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/EmbeddedServiceMethodBinderT.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/EmbeddedThreadI.h \ /home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/EmbeddedThreadI.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/EmbeddedThread.h \ /home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/EmbeddedThread.h
../../../..//Source/Components/Interfaces/UDPStream/UDPSTransportI.h
@@ -136,5 +136,4 @@ UDPStreamerClient.o: UDPStreamerClient.cpp \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/EmbeddedServiceMethodBinderI.h \ /home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/EmbeddedServiceMethodBinderI.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/EmbeddedServiceMethodBinderT.h \ /home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/EmbeddedServiceMethodBinderT.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/EmbeddedThreadI.h \ /home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/EmbeddedThreadI.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/EmbeddedThread.h \ /home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/EmbeddedThread.h
../../../..//Source/Components/Interfaces/UDPStream/UDPSTransportI.h
@@ -674,14 +674,6 @@ bool DebugService::SendUDPSConfig() {
payloadOffset++; payloadOffset++;
} }
// Write this host's HRT tick rate: DATA packets carry raw counter values,
// which a receiver on another machine cannot convert to seconds without it.
if ((payloadOffset + 8u) <= CFG_BUF_SIZE) {
uint64 hrtFrequency = HighResolutionTimer::Frequency();
memcpy(payload + payloadOffset, &hrtFrequency, 8u);
payloadOffset += 8u;
}
udpsNumSlots = newNumSlots; udpsNumSlots = newNumSlots;
mutex.FastUnLock(); mutex.FastUnLock();
@@ -1 +0,0 @@
include Makefile.inc
@@ -1,33 +0,0 @@
#############################################################
# MARTe2 Integrated Components — TCPStreamService
#############################################################
OBJSX=TCPSFrameQueue.x TCPSServiceStream.x TCPSWorker.x TCPStreamService.x TCPStreamJoin.x
PACKAGE=Components/Interfaces
ROOT_DIR=../../../../
MAKEDEFAULTDIR=$(MARTe2_DIR)/MakeDefaults
include $(MAKEDEFAULTDIR)/MakeStdLibDefs.$(TARGET)
INCLUDES += -I.
INCLUDES += -I$(ROOT_DIR)/Common/UDP
INCLUDES += -I$(ROOT_DIR)/Source/Components/Interfaces/UDPStream
INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L0Types
INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L1Portability
INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L2Objects
INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L3Streams
INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L4Messages
INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L4Configuration
INCLUDES += -I$(MARTe2_DIR)/Source/Core/Scheduler/L1Portability
INCLUDES += -I$(MARTe2_DIR)/Source/Core/Scheduler/L3Services
INCLUDES += -I$(MARTe2_DIR)/Source/Core/FileSystem/L1Portability
INCLUDES += -I$(MARTe2_DIR)/Source/Core/FileSystem/L3Streams
all: $(OBJS) $(SUBPROJ) \
$(BUILD_DIR)/TCPStreamService$(LIBEXT) \
$(BUILD_DIR)/TCPStreamService$(DLLEXT)
echo $(OBJS)
include depends.$(TARGET)
include $(MAKEDEFAULTDIR)/MakeStdLibRules.$(TARGET)
@@ -1,70 +0,0 @@
#ifndef TCPS_CONN_H_
#define TCPS_CONN_H_
/**
* @file TCPSConn.h
* @brief One accepted connection of a TCPStreamService (internal).
*
* Ownership: created by the acceptor thread, then owned and deleted by exactly
* one TCPSWorker. `streams` and `closeRequested` are the only fields touched
* from other threads; `stream` is guarded by the service topology lock, the
* queue by `qMutex`.
*/
#include "CompilerTypes.h"
#include "FastPollingMutexSem.h"
#include "TCPSFrameQueue.h"
#include "TCPStreamService.h"
#include "UDPSProtocol.h"
#include <unistd.h>
namespace MARTe {
class TCPSWorker;
class TCPSServiceStream;
struct TCPSConn {
TCPSConn(int socketFd, TCPSWorker *owner, uint32 queueDepth, uint64 handshakeDeadline)
: fd(socketFd), worker(owner), subscribed(false), hsHave(0u),
hsWant(UDPS_HEADER_SIZE), hsHeaderOk(false), hsDeadline(handshakeDeadline),
numStreams(0u), closeRequested(false) {
for (uint32 i = 0u; i < TCPS_MAX_CONN_STREAMS; i++) {
streams[i] = NULL_PTR(TCPSServiceStream *);
}
q.Init(queueDepth);
}
~TCPSConn() {
if (fd >= 0) {
(void)close(fd);
}
}
int fd;
TCPSWorker *worker;
/* Worker-thread only. */
bool subscribed;
uint8 hs[UDPS_HEADER_SIZE + TCPS_MAX_SUBSCRIBE_PAYLOAD + 1u];
uint32 hsHave;
uint32 hsWant;
bool hsHeaderOk;
uint64 hsDeadline;
/* Service topology lock. */
TCPSServiceStream *streams[TCPS_MAX_CONN_STREAMS]; ///< index = stream index on the wire
uint32 numStreams;
volatile bool closeRequested;
/* qMutex. */
FastPollingMutexSem qMutex;
TCPSFrameQueue q;
private:
TCPSConn(const TCPSConn &);
TCPSConn &operator=(const TCPSConn &);
};
} // namespace MARTe
#endif // TCPS_CONN_H_
@@ -1,204 +0,0 @@
/**
* @file TCPSFrameQueue.cpp
* @brief Implementation of TCPSFrameQueue.
*/
#include "TCPSFrameQueue.h"
#include "HighResolutionTimer.h"
#include <errno.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/uio.h>
#include <sys/types.h>
namespace MARTe {
struct TCPSFrameQueue::Slot {
Slot() : buf(NULL_PTR(uint8 *)), cap(0u), len(0u), isConfig(false), stream(0u) {}
~Slot() {
if (buf != NULL_PTR(uint8 *)) {
delete[] buf;
}
}
uint8 *buf;
uint32 cap;
uint32 len;
bool isConfig;
uint16 stream;
};
/** magic(4) + type(1) + counter(4): offset of the header's fragmentIdx field. */
static const uint32 TCPS_FRAGIDX_OFFSET = 9u;
TCPSFrameQueue::TCPSFrameQueue()
: slots(NULL_PTR(Slot **)), cap(0u), depth(0u), count(0u), sent(0u), lastProgress(0u) {
}
TCPSFrameQueue::~TCPSFrameQueue() {
if (slots != NULL_PTR(Slot **)) {
for (uint32 i = 0u; i < cap; i++) {
delete slots[i];
}
delete[] slots;
}
}
void TCPSFrameQueue::Init(uint32 d) {
depth = d;
cap = d + 2u;
slots = new Slot *[cap];
for (uint32 i = 0u; i < cap; i++) {
slots[i] = new Slot();
}
count = 0u;
sent = 0u;
lastProgress = HighResolutionTimer::Counter();
}
void TCPSFrameQueue::Reserve(uint32 numStreams) {
const uint32 want = numStreams * (depth + 2u);
if (want > cap) {
Slot **grown = new Slot *[want];
for (uint32 i = 0u; i < cap; i++) {
grown[i] = slots[i];
}
for (uint32 i = cap; i < want; i++) {
grown[i] = new Slot();
}
delete[] slots;
slots = grown;
cap = want;
}
}
/** Remove slot @p i, keeping order; the slot object is recycled at the end. */
void TCPSFrameQueue::RemoveAt(uint32 i) {
Slot *v = slots[i];
for (uint32 j = i + 1u; j < count; j++) {
slots[j - 1u] = slots[j];
}
slots[count - 1u] = v;
count--;
}
bool TCPSFrameQueue::Push(uint16 streamIdx, bool isConfig, const uint8 *frame,
uint32 frameBytes, uint32 &dropped) {
const uint32 firstFree = (sent > 0u) ? 1u : 0u; // slots below this are on the wire
if (isConfig) {
uint32 i = firstFree;
while (i < count) {
if (slots[i]->stream == streamIdx) {
if (!slots[i]->isConfig) {
dropped++;
}
RemoveAt(i);
} else {
i++;
}
}
} else {
uint32 dataCount = 0u;
for (uint32 i = 0u; i < count; i++) {
if ((slots[i]->stream == streamIdx) && !slots[i]->isConfig) {
dataCount++;
}
}
if (dataCount >= depth) {
uint32 victim = count;
for (uint32 i = firstFree; (i < count) && (victim == count); i++) {
if ((slots[i]->stream == streamIdx) && !slots[i]->isConfig) {
victim = i;
}
}
dropped++;
if (victim == count) {
return false; // only the frame on the wire is queued
}
RemoveAt(victim);
}
}
if (count >= cap) {
dropped++;
return false;
}
Slot &s = *slots[count];
if (s.cap < frameBytes) {
if (s.buf != NULL_PTR(uint8 *)) {
delete[] s.buf;
}
s.buf = new uint8[frameBytes];
s.cap = frameBytes;
}
memcpy(s.buf, frame, frameBytes);
if (frameBytes >= (TCPS_FRAGIDX_OFFSET + 2u)) {
memcpy(&s.buf[TCPS_FRAGIDX_OFFSET], &streamIdx, 2u);
}
s.len = frameBytes;
s.isConfig = isConfig;
s.stream = streamIdx;
if (count == 0u) {
sent = 0u;
lastProgress = HighResolutionTimer::Counter();
}
count++;
return true;
}
/** Account for @p bytes written from the head of the queue. */
void TCPSFrameQueue::Consume(uint32 bytes) {
while ((bytes > 0u) && (count > 0u)) {
const uint32 left = slots[0]->len - sent;
if (bytes >= left) {
bytes -= left;
RemoveAt(0u);
sent = 0u;
} else {
sent += bytes;
bytes = 0u;
}
}
}
bool TCPSFrameQueue::Flush(int fd) {
bool alive = true;
while (alive && (count > 0u)) {
struct iovec iov[TCPS_MAX_BATCH];
uint32 n = (count < TCPS_MAX_BATCH) ? count : TCPS_MAX_BATCH;
for (uint32 i = 0u; i < n; i++) {
const uint32 off = (i == 0u) ? sent : 0u;
iov[i].iov_base = &slots[i]->buf[off];
iov[i].iov_len = slots[i]->len - off;
}
struct msghdr msg;
memset(&msg, 0, sizeof(msg));
msg.msg_iov = iov;
msg.msg_iovlen = n;
ssize_t w = sendmsg(fd, &msg, MSG_NOSIGNAL | MSG_DONTWAIT);
if (w > 0) {
lastProgress = HighResolutionTimer::Counter();
Consume(static_cast<uint32>(w));
} else if ((w < 0) && ((errno == EAGAIN) || (errno == EWOULDBLOCK))) {
break;
} else if ((w < 0) && (errno == EINTR)) {
continue;
} else {
alive = false;
}
}
return alive;
}
bool TCPSFrameQueue::Empty() const {
return count == 0u;
}
uint64 TCPSFrameQueue::LastProgress() const {
return lastProgress;
}
} // namespace MARTe
@@ -1,79 +0,0 @@
#ifndef TCPS_FRAME_QUEUE_H_
#define TCPS_FRAME_QUEUE_H_
/**
* @file TCPSFrameQueue.h
* @brief Bounded per-connection output queue of UDPS frames (header + payload).
*
* Same semantics as the per-client queue inside TCPSServer: at most `depth`
* DATA frames wait; when full the oldest DATA frame that is not already
* (partially) on the wire is discarded and counted. A CONFIG is never dropped
* and, when queued, discards the DATA frames behind the one on the wire (they
* were encoded for the previous layout).
*
* Not thread-safe: the owner serialises Push() / Flush().
*/
#include "CompilerTypes.h"
namespace MARTe {
class TCPSFrameQueue {
public:
TCPSFrameQueue();
~TCPSFrameQueue();
/** Allocate `depth` DATA slots plus two CONFIG slots (room for one stream). */
void Init(uint32 depth);
/**
* Make room for @p numStreams multiplexed streams: each gets its own `depth`
* DATA frames plus two CONFIG slots. Grow-only; call while the queue is idle
* (before the connection is attached to any stream).
*/
void Reserve(uint32 numStreams);
/**
* Append a complete frame (copied). The frame is stamped with @p streamIdx in
* its header's fragmentIdx field. Accounting is per stream: a full stream
* evicts its own oldest DATA, and a CONFIG only discards the queued frames
* of its own stream.
* @param[in,out] dropped incremented by the number of DATA frames discarded.
* @return false if the frame itself was refused (queue full of in-flight data).
*/
bool Push(uint16 streamIdx, bool isConfig, const uint8 *frame, uint32 frameBytes,
uint32 &dropped);
/**
* Write as much as the socket accepts without blocking, batching up to
* TCPS_MAX_BATCH frames per system call.
* @return false if the connection is broken.
*/
bool Flush(int fd);
/** Most frames sent by one sendmsg(). */
static const uint32 TCPS_MAX_BATCH = 16u;
bool Empty() const;
/** HRT ticks of the last byte written (or of the queue becoming non-empty). */
uint64 LastProgress() const;
private:
struct Slot;
TCPSFrameQueue(const TCPSFrameQueue &);
TCPSFrameQueue &operator=(const TCPSFrameQueue &);
void RemoveAt(uint32 i);
void Consume(uint32 bytes);
Slot **slots;
uint32 cap;
uint32 depth;
uint32 count;
uint32 sent;
uint64 lastProgress;
};
} // namespace MARTe
#endif // TCPS_FRAME_QUEUE_H_
@@ -1,329 +0,0 @@
/**
* @file TCPSServiceStream.cpp
* @brief Implementation of TCPSServiceStream.
*/
#define DLL_API
#include "TCPSServiceStream.h"
#include "AdvancedErrorManagement.h"
#include "ObjectRegistryDatabase.h"
#include "TCPSConn.h"
#include "TCPSWorker.h"
#include <string.h>
namespace MARTe {
const uint32 TCPSServiceStream::TCPSS_DEFAULT_MAX_PAYLOAD;
static const uint32 TCPSS_MAX_FRAME_PAYLOAD = 1048576u;
TCPSServiceStream::TCPSServiceStream(const char8 *serviceObjectName, const char8 *name)
: serviceName(serviceObjectName), streamName(name), registered(false),
maxPayloadSize(TCPSS_DEFAULT_MAX_PAYLOAD), subs(NULL_PTR(TCPSConn **)), subIdx(NULL_PTR(uint16 *)),
capSubs(0u), numSubs(0u), cachedConfigFrame(NULL_PTR(uint8 *)),
cachedConfigBytes(0u), cachedConfigCap(0u), configCounter(0u), drops(0u),
numTaps(0u), dataFrame(NULL_PTR(uint8 *)), dataFrameCap(0u) {
for (uint32 i = 0u; i < TCPSS_MAX_TAPS; i++) {
taps[i] = NULL_PTR(TCPSStreamTapI *);
}
}
TCPSServiceStream::~TCPSServiceStream() {
(void)Stop();
if (cachedConfigFrame != NULL_PTR(uint8 *)) {
delete[] cachedConfigFrame;
}
if (dataFrame != NULL_PTR(uint8 *)) {
delete[] dataFrame;
}
if (subs != NULL_PTR(TCPSConn **)) {
delete[] subs;
}
if (subIdx != NULL_PTR(uint16 *)) {
delete[] subIdx;
}
}
bool TCPSServiceStream::Initialise(StructuredDataI &data) {
bool ok = true;
maxPayloadSize = TCPSS_DEFAULT_MAX_PAYLOAD;
(void)data.Read("MaxPayloadSize", maxPayloadSize);
if ((maxPayloadSize == 0u) || (maxPayloadSize > TCPSS_MAX_FRAME_PAYLOAD)) {
REPORT_ERROR_STATIC(ErrorManagement::ParametersError,
"TCPSServiceStream: MaxPayloadSize %u must be in 1..%u.",
maxPayloadSize, TCPSS_MAX_FRAME_PAYLOAD);
ok = false;
}
if ((streamName.Size() > 0u) && (strchr(streamName.Buffer(), ',') != NULL_PTR(char8 *))) {
REPORT_ERROR_STATIC(ErrorManagement::ParametersError,
"TCPSServiceStream: StreamName '%s' may not contain a comma.",
streamName.Buffer());
ok = false;
}
if ((streamName.Size() == 0u) || (streamName.Size() > TCPS_MAX_STREAM_NAME)) {
REPORT_ERROR_STATIC(ErrorManagement::ParametersError,
"TCPSServiceStream: StreamName must be 1..%u characters.",
TCPS_MAX_STREAM_NAME);
ok = false;
}
return ok;
}
bool TCPSServiceStream::Start() {
if (registered) {
return true;
}
if (!service.IsValid()) {
ReferenceT<TCPStreamService> found =
ObjectRegistryDatabase::Instance()->Find(serviceName.Buffer());
if (!found.IsValid()) {
REPORT_ERROR_STATIC(ErrorManagement::InitialisationError,
"TCPSServiceStream: no TCPStreamService named '%s'.",
serviceName.Buffer());
return false;
}
service = found;
}
mutex.FastLock();
if (subs == NULL_PTR(TCPSConn **)) {
capSubs = service->MaxClients();
subs = new TCPSConn *[capSubs];
subIdx = new uint16[capSubs];
}
numSubs = 0u;
mutex.FastUnLock();
registered = service->RegisterStream(this);
return registered;
}
bool TCPSServiceStream::Stop() {
if (registered) {
service->UnregisterStream(this); // drops every subscriber
registered = false;
}
mutex.FastLock();
if (cachedConfigFrame != NULL_PTR(uint8 *)) {
cachedConfigBytes = 0u;
}
mutex.FastUnLock();
return true;
}
void TCPSServiceStream::ServiceClients() {
}
bool TCPSServiceStream::BuildFrame(uint8 *&buf, uint32 &cap, uint8 type, uint32 counter,
const uint8 *payload, uint32 payloadSize,
uint32 &frameBytes) {
bool ok = (payloadSize <= TCPSS_MAX_FRAME_PAYLOAD);
if (ok) {
const uint32 need = UDPS_HEADER_SIZE + payloadSize;
if (cap < need) {
if (buf != NULL_PTR(uint8 *)) {
delete[] buf;
}
buf = new uint8[need];
cap = need;
}
UDPSBuildHeader(buf, type, counter, 0u, 1u, payloadSize);
if (payloadSize > 0u) {
memcpy(&buf[UDPS_HEADER_SIZE], payload, payloadSize);
}
frameBytes = need;
}
return ok;
}
/** Caller holds `mutex`. */
void TCPSServiceStream::Distribute(bool isConfig, const uint8 *frame, uint32 frameBytes) {
for (uint32 i = 0u; i < numSubs; i++) {
TCPSConn *c = subs[i];
uint32 dropped = 0u;
c->qMutex.FastLock();
const bool wasEmpty = c->q.Empty();
(void)c->q.Push(subIdx[i], isConfig, frame, frameBytes, dropped);
c->qMutex.FastUnLock();
drops += dropped;
if (wasEmpty) {
c->worker->Wake();
}
}
}
bool TCPSServiceStream::SendConfig(const uint8 *payload, uint32 payloadSize) {
if (!registered) {
return false;
}
if (payloadSize > TCPSS_MAX_FRAME_PAYLOAD) {
REPORT_ERROR_STATIC(ErrorManagement::ParametersError,
"TCPSServiceStream: CONFIG of %u bytes exceeds the frame limit.",
payloadSize);
return false;
}
mutex.FastLock();
configCounter++;
bool ok = BuildFrame(cachedConfigFrame, cachedConfigCap, UDPS_TYPE_CONFIG, configCounter,
payload, payloadSize, cachedConfigBytes);
if (ok) {
Distribute(true, cachedConfigFrame, cachedConfigBytes);
}
mutex.FastUnLock();
if (ok && (numTaps > 0u)) {
tapMutex.FastLock();
for (uint32 i = 0u; i < numTaps; i++) {
taps[i]->OnTapConfig(this, payload, payloadSize);
}
tapMutex.FastUnLock();
}
return ok;
}
bool TCPSServiceStream::SendData(uint32 counter, const uint8 *payload, uint32 payloadSize) {
if (!registered) {
return false;
}
if (numTaps > 0u) {
tapMutex.FastLock();
for (uint32 i = 0u; i < numTaps; i++) {
taps[i]->OnTapData(this, counter, payload, payloadSize);
}
tapMutex.FastUnLock();
}
if (numSubs == 0u) {
return true;
}
uint32 frameBytes = 0u;
if (!BuildFrame(dataFrame, dataFrameCap, UDPS_TYPE_DATA, counter, payload, payloadSize,
frameBytes)) {
REPORT_ERROR_STATIC(ErrorManagement::ParametersError,
"TCPSServiceStream: DATA of %u bytes exceeds the frame limit.",
payloadSize);
return false;
}
mutex.FastLock();
Distribute(false, dataFrame, frameBytes);
mutex.FastUnLock();
return true;
}
bool TCPSServiceStream::AddTap(TCPSStreamTapI *tap) {
bool ok = false;
uint8 *cfg = NULL_PTR(uint8 *);
uint32 cfgBytes = 0u;
mutex.FastLock();
if (cachedConfigBytes > UDPS_HEADER_SIZE) {
cfgBytes = cachedConfigBytes - UDPS_HEADER_SIZE;
cfg = new uint8[cfgBytes];
memcpy(cfg, &cachedConfigFrame[UDPS_HEADER_SIZE], cfgBytes);
}
mutex.FastUnLock();
tapMutex.FastLock();
if (numTaps < TCPSS_MAX_TAPS) {
taps[numTaps] = tap;
numTaps = numTaps + 1u;
ok = true;
if (cfg != NULL_PTR(uint8 *)) {
tap->OnTapConfig(this, cfg, cfgBytes);
}
}
tapMutex.FastUnLock();
if (cfg != NULL_PTR(uint8 *)) {
delete[] cfg;
}
return ok;
}
void TCPSServiceStream::RemoveTap(TCPSStreamTapI *tap) {
tapMutex.FastLock();
for (uint32 i = 0u; i < numTaps; i++) {
if (taps[i] == tap) {
taps[i] = taps[numTaps - 1u];
numTaps = numTaps - 1u;
break;
}
}
tapMutex.FastUnLock();
}
bool TCPSServiceStream::HasClients() const {
/* A tap (TCPStreamJoin) consumes frames like a subscriber: the producer must keep publishing. */
return (numSubs > 0u) || (numTaps > 0u);
}
bool TCPSServiceStream::IsMulticast() const {
return false;
}
uint16 TCPSServiceStream::GetPort() const {
return service.IsValid() ? service->GetBoundPort() : static_cast<uint16>(0u);
}
const char8 *TCPSServiceStream::GetStreamName() const {
return streamName.Buffer();
}
uint32 TCPSServiceStream::GetClientCount() const {
return numSubs;
}
uint64 TCPSServiceStream::GetClientDrops() const {
return drops;
}
uint32 TCPSServiceStream::GetMaxPayloadSize() const {
return maxPayloadSize;
}
bool TCPSServiceStream::AttachConn(TCPSConn *c, uint16 streamIdx) {
bool ok = false;
mutex.FastLock();
if (numSubs < capSubs) {
// The CONFIG goes in first so the client always decodes DATA against it.
if (cachedConfigBytes > 0u) {
uint32 dropped = 0u;
c->qMutex.FastLock();
(void)c->q.Push(streamIdx, true, cachedConfigFrame, cachedConfigBytes, dropped);
c->qMutex.FastUnLock();
}
subs[numSubs] = c;
subIdx[numSubs] = streamIdx;
numSubs = numSubs + 1u;
ok = true;
}
mutex.FastUnLock();
return ok;
}
void TCPSServiceStream::DetachConn(TCPSConn *c) {
mutex.FastLock();
for (uint32 i = 0u; i < numSubs; i++) {
if (subs[i] == c) {
subs[i] = subs[numSubs - 1u];
subIdx[i] = subIdx[numSubs - 1u];
numSubs = numSubs - 1u;
break;
}
}
mutex.FastUnLock();
}
void TCPSServiceStream::DropAllConns() {
mutex.FastLock();
for (uint32 i = 0u; i < numSubs; i++) {
TCPSConn *c = subs[i];
for (uint32 k = 0u; k < c->numStreams; k++) {
if (c->streams[k] == this) {
c->streams[k] = NULL_PTR(TCPSServiceStream *);
}
}
subs[i]->closeRequested = true;
subs[i]->worker->Wake();
}
numSubs = 0u;
mutex.FastUnLock();
}
} // namespace MARTe
@@ -1,110 +0,0 @@
#ifndef TCPS_SERVICE_STREAM_H_
#define TCPS_SERVICE_STREAM_H_
/**
* @file TCPSServiceStream.h
* @brief UDPSServerI handle that publishes one named stream through a
* TCPStreamService instead of through a private listening socket.
*
* It is what a TCPStreamer uses when its `Service` key is set. All socket work
* (accept, handshake, flush, eviction) is done by the service's threads; the
* producer thread only builds the frame and copies it into the bounded queue of
* every subscribed connection (SendData never blocks on a socket).
*/
#include "FastPollingMutexSem.h"
#include "ReferenceT.h"
#include "StreamString.h"
#include "MutexSem.h"
#include "TCPSStreamTapI.h"
#include "TCPStreamService.h"
#include "UDPSProtocol.h"
#include "UDPSTransportI.h"
namespace MARTe {
struct TCPSConn;
class TCPSServiceStream : public UDPSServerI {
public:
/** Default maximum payload bytes per frame (matches TCPSServer). */
static const uint32 TCPSS_DEFAULT_MAX_PAYLOAD = 60000u;
TCPSServiceStream(const char8 *serviceObjectName, const char8 *streamName);
virtual ~TCPSServiceStream();
/** Keys: MaxPayloadSize (uint32, 1..1 MiB, default 60000). */
virtual bool Initialise(StructuredDataI &data);
/** Resolve the service by name and register the stream (fails on a duplicate name). */
virtual bool Start();
/** Unregister and drop every subscriber (idempotent). */
virtual bool Stop();
/** No-op: the service threads do the housekeeping. */
virtual void ServiceClients();
virtual bool SendConfig(const uint8 *payload, uint32 payloadSize);
virtual bool SendData(uint32 counter, const uint8 *payload, uint32 payloadSize);
virtual bool HasClients() const;
virtual bool IsMulticast() const;
/** @return the service's bound port (0 while the service is unresolved). */
virtual uint16 GetPort() const;
const char8 *GetStreamName() const;
uint32 GetClientCount() const;
/** @return DATA frames discarded for slow subscribers (all time). */
uint64 GetClientDrops() const;
uint32 GetMaxPayloadSize() const;
/** Most taps one stream can carry. */
static const uint32 TCPSS_MAX_TAPS = 4u;
/**
* Observe every CONFIG / DATA this stream publishes. The cached CONFIG (if any) is
* replayed to the new tap before AddTap returns. Taps run on the producer thread.
* @return false if the tap table is full.
*/
bool AddTap(TCPSStreamTapI *tap);
/** Remove a tap; once it returns no callback is running or will start. */
void RemoveTap(TCPSStreamTapI *tap);
/* ---- called by TCPStreamService with its topology lock held ---- */
/** Add a connection and prime it with the cached CONFIG. @return false if full. */
bool AttachConn(TCPSConn *c, uint16 streamIdx);
void DetachConn(TCPSConn *c);
/** Ask every subscriber to close and forget them. */
void DropAllConns();
private:
bool BuildFrame(uint8 *&buf, uint32 &cap, uint8 type, uint32 counter,
const uint8 *payload, uint32 payloadSize, uint32 &frameBytes);
void Distribute(bool isConfig, const uint8 *frame, uint32 frameBytes);
StreamString serviceName;
mutable StreamString streamName;
ReferenceT<TCPStreamService> service;
bool registered;
uint32 maxPayloadSize;
FastPollingMutexSem mutex; ///< guards subs, numSubs, cachedConfig*, drops
TCPSConn **subs;
uint16 *subIdx; ///< stream index each subscriber knows this stream by
uint32 capSubs;
volatile uint32 numSubs;
uint8 *cachedConfigFrame;
uint32 cachedConfigBytes;
uint32 cachedConfigCap;
uint32 configCounter;
uint64 drops;
FastPollingMutexSem tapMutex; ///< held while a tap callback runs
TCPSStreamTapI *taps[TCPSS_MAX_TAPS];
volatile uint32 numTaps;
uint8 *dataFrame; ///< producer-thread scratch
uint32 dataFrameCap;
TCPSServiceStream(const TCPSServiceStream &);
TCPSServiceStream &operator=(const TCPSServiceStream &);
};
} // namespace MARTe
#endif // TCPS_SERVICE_STREAM_H_
@@ -1,40 +0,0 @@
#ifndef TCPS_STREAM_TAP_I_H_
#define TCPS_STREAM_TAP_I_H_
/**
* @file TCPSStreamTapI.h
* @brief Hooks used by TCPStreamJoin to follow the streams of a TCPStreamService.
*
* TCPSStreamTapI observes the CONFIG / DATA payloads a stream publishes (on the
* producer's thread, after the frame went to the subscribers). TCPSStreamObserverI
* observes streams registering with / leaving a service.
*/
#include "CompilerTypes.h"
namespace MARTe {
class TCPSServiceStream;
class TCPSStreamTapI {
public:
virtual ~TCPSStreamTapI() {}
/** A CONFIG payload was published (or the cached one is replayed on AddTap). */
virtual void OnTapConfig(TCPSServiceStream *source, const uint8 *payload, uint32 payloadSize) = 0;
/** A DATA payload was published. */
virtual void OnTapData(TCPSServiceStream *source, uint32 counter, const uint8 *payload,
uint32 payloadSize) = 0;
};
class TCPSStreamObserverI {
public:
virtual ~TCPSStreamObserverI() {}
/** @p stream just registered. May call back into the service. */
virtual void OnStreamRegistered(TCPSServiceStream *stream) = 0;
/** @p stream is leaving (it is still valid during the call). */
virtual void OnStreamUnregistered(TCPSServiceStream *stream) = 0;
};
} // namespace MARTe
#endif // TCPS_STREAM_TAP_I_H_
@@ -1,266 +0,0 @@
/**
* @file TCPSWorker.cpp
* @brief Implementation of TCPSWorker.
*/
#define DLL_API
#include "TCPSWorker.h"
#include "AdvancedErrorManagement.h"
#include "Atomic.h"
#include "HighResolutionTimer.h"
#include "TCPSConn.h"
#include "TCPStreamService.h"
#include "UDPSProtocol.h"
#include <errno.h>
#include <fcntl.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>
namespace MARTe {
/** Longest wait in poll(): bounds Stop() latency and the stall / handshake checks. */
static const int TCPS_WORKER_SLICE_MS = 50;
TCPSWorker::TCPSWorker(TCPStreamService &owner, uint32 maxConnections)
: service(owner), maxConns(maxConnections), thread(*this),
incoming(new TCPSConn *[maxConnections]), numIncoming(0u),
conns(new TCPSConn *[maxConnections]), numConns(0u),
pfds(new struct pollfd[maxConnections + 1u]), load(0) {
wakeFd[0] = -1;
wakeFd[1] = -1;
if (pipe(wakeFd) == 0) {
for (int i = 0; i < 2; i++) {
int flags = fcntl(wakeFd[i], F_GETFL, 0);
if (flags >= 0) {
(void)fcntl(wakeFd[i], F_SETFL, flags | O_NONBLOCK);
}
}
}
}
TCPSWorker::~TCPSWorker() {
Stop();
CloseAll();
for (uint32 i = 0u; i < numIncoming; i++) {
delete incoming[i];
service.ConnClosed();
}
delete[] incoming;
delete[] conns;
delete[] pfds;
for (int i = 0; i < 2; i++) {
if (wakeFd[i] >= 0) {
(void)close(wakeFd[i]);
}
}
}
bool TCPSWorker::Start(uint32 cpuMask, uint32 stackSize) {
if (wakeFd[0] < 0) {
return false;
}
thread.SetCPUMask(cpuMask);
thread.SetStackSize(stackSize);
return thread.Start() == ErrorManagement::NoError;
}
void TCPSWorker::Stop() {
(void)thread.Stop();
}
bool TCPSWorker::AddIncoming(TCPSConn *c) {
bool ok = false;
incomingMutex.FastLock();
if (numIncoming < maxConns) {
incoming[numIncoming] = c;
numIncoming++;
ok = true;
}
incomingMutex.FastUnLock();
if (ok) {
Atomic::Increment(&load);
Wake();
}
return ok;
}
void TCPSWorker::Wake() {
if (wakeFd[1] >= 0) {
const uint8 b = 1u;
// EAGAIN means the pipe is already full of wake-ups: nothing to add.
ssize_t r = write(wakeFd[1], &b, 1u);
(void)r;
}
}
int32 TCPSWorker::Load() const {
return load;
}
void TCPSWorker::AdoptIncoming() {
incomingMutex.FastLock();
for (uint32 i = 0u; i < numIncoming; i++) {
conns[numConns] = incoming[i];
numConns++;
}
numIncoming = 0u;
incomingMutex.FastUnLock();
}
void TCPSWorker::CloseConn(TCPSConn *c) {
service.Detach(c);
delete c; // closes the socket
service.ConnClosed();
Atomic::Decrement(&load);
}
void TCPSWorker::CloseAll() {
for (uint32 i = 0u; i < numConns; i++) {
CloseConn(conns[i]);
}
numConns = 0u;
}
bool TCPSWorker::Drain(TCPSConn &c) {
uint8 scratch[64];
bool alive = true;
for (;;) {
ssize_t n = recv(c.fd, scratch, sizeof(scratch), MSG_DONTWAIT);
if (n > 0) {
continue; // clients have nothing more to say after SUBSCRIBE
}
if (n == 0) {
alive = false;
} else if ((errno == EAGAIN) || (errno == EWOULDBLOCK)) {
alive = true;
} else if (errno == EINTR) {
continue;
} else {
alive = false;
}
break;
}
return alive;
}
bool TCPSWorker::HandshakeStep(TCPSConn &c) {
ssize_t n = recv(c.fd, &c.hs[c.hsHave], c.hsWant - c.hsHave, MSG_DONTWAIT);
if (n == 0) {
return false;
}
if (n < 0) {
return (errno == EAGAIN) || (errno == EWOULDBLOCK) || (errno == EINTR);
}
c.hsHave += static_cast<uint32>(n);
if (!c.hsHeaderOk && (c.hsHave >= UDPS_HEADER_SIZE)) {
UDPSPacketHeader h;
memcpy(&h, c.hs, UDPS_HEADER_SIZE);
if ((h.magic != UDPS_MAGIC) || (h.type != UDPS_TYPE_SUBSCRIBE) ||
(h.payloadBytes == 0u) || (h.payloadBytes > TCPS_MAX_SUBSCRIBE_PAYLOAD)) {
REPORT_ERROR_STATIC(ErrorManagement::Warning,
"TCPStreamService: connection did not open with a valid SUBSCRIBE; closing.");
service.NoteRejected();
return false;
}
c.hsHeaderOk = true;
c.hsWant = UDPS_HEADER_SIZE + h.payloadBytes;
}
if (c.hsHeaderOk && (c.hsHave == c.hsWant)) {
c.hs[c.hsWant] = 0u;
char8 *names = reinterpret_cast<char8 *>(&c.hs[UDPS_HEADER_SIZE]);
if (!service.Attach(&c, names)) {
REPORT_ERROR_STATIC(ErrorManagement::Warning,
"TCPStreamService: cannot attach to '%s'; closing.", names);
service.NoteRejected();
return false;
}
c.subscribed = true;
}
return true;
}
ErrorManagement::ErrorType TCPSWorker::Execute(ExecutionInfo &info) {
if (info.GetStage() == ExecutionInfo::TerminationStage) {
return ErrorManagement::NoError;
}
if (info.GetStage() != ExecutionInfo::MainStage) {
return ErrorManagement::NoError;
}
AdoptIncoming();
pfds[0].fd = wakeFd[0];
pfds[0].events = POLLIN;
pfds[0].revents = 0;
for (uint32 i = 0u; i < numConns; i++) {
TCPSConn *c = conns[i];
short ev = POLLIN;
if (c->subscribed) {
c->qMutex.FastLock();
if (!c->q.Empty()) {
ev = static_cast<short>(ev | POLLOUT);
}
c->qMutex.FastUnLock();
}
pfds[i + 1u].fd = c->fd;
pfds[i + 1u].events = ev;
pfds[i + 1u].revents = 0;
}
int pr = poll(pfds, static_cast<nfds_t>(numConns + 1u), TCPS_WORKER_SLICE_MS);
if (pr < 0) {
return ErrorManagement::NoError; // EINTR
}
if ((pfds[0].revents & POLLIN) != 0) {
uint8 sink[64];
while (read(wakeFd[0], sink, sizeof(sink)) > 0) {
}
}
const uint64 now = HighResolutionTimer::Counter();
const uint64 stallTicks = service.StallTicks();
uint32 kept = 0u;
for (uint32 i = 0u; i < numConns; i++) {
TCPSConn *c = conns[i];
const short re = pfds[i + 1u].revents;
bool keep = !c->closeRequested && ((re & (POLLERR | POLLNVAL)) == 0);
if (keep && ((re & (POLLIN | POLLHUP)) != 0)) {
keep = c->subscribed ? Drain(*c) : HandshakeStep(*c);
}
if (keep && c->subscribed) {
c->qMutex.FastLock();
keep = c->q.Flush(c->fd);
if (keep && !c->q.Empty() && (stallTicks > 0u) &&
(now > c->q.LastProgress()) && ((now - c->q.LastProgress()) > stallTicks)) {
REPORT_ERROR_STATIC(ErrorManagement::Warning,
"TCPStreamService: client stalled; disconnecting.");
service.NoteEviction();
keep = false;
}
c->qMutex.FastUnLock();
}
if (keep && !c->subscribed && (now > c->hsDeadline)) {
REPORT_ERROR_STATIC(ErrorManagement::Warning,
"TCPStreamService: SUBSCRIBE timeout; closing.");
service.NoteRejected();
keep = false;
}
if (keep) {
conns[kept] = c;
kept++;
} else {
CloseConn(c);
}
}
numConns = kept;
return ErrorManagement::NoError;
}
} // namespace MARTe
@@ -1,68 +0,0 @@
#ifndef TCPS_WORKER_H_
#define TCPS_WORKER_H_
/**
* @file TCPSWorker.h
* @brief One I/O thread of a TCPStreamService (internal).
*
* Owns a set of connections, polls them (plus a self-pipe used by producers to
* announce new frames), performs the SUBSCRIBE handshake, flushes the bounded
* queues and evicts stalled / dead peers.
*/
#include "EmbeddedServiceMethodBinderI.h"
#include "ExecutionInfo.h"
#include "FastPollingMutexSem.h"
#include "SingleThreadService.h"
#include <poll.h>
namespace MARTe {
class TCPStreamService;
struct TCPSConn;
class TCPSWorker : public EmbeddedServiceMethodBinderI {
public:
TCPSWorker(TCPStreamService &owner, uint32 maxConns);
virtual ~TCPSWorker();
bool Start(uint32 cpuMask, uint32 stackSize);
void Stop();
/** Hand a freshly accepted connection to this worker (acceptor thread). */
bool AddIncoming(TCPSConn *c);
/** Interrupt the poll so queued data is flushed now (any thread, cheap). */
void Wake();
/** @return connections currently assigned (approximate). */
int32 Load() const;
virtual ErrorManagement::ErrorType Execute(ExecutionInfo &info);
private:
void AdoptIncoming();
bool HandshakeStep(TCPSConn &c);
bool Drain(TCPSConn &c);
void CloseConn(TCPSConn *c);
void CloseAll();
TCPStreamService &service;
uint32 maxConns;
SingleThreadService thread;
int wakeFd[2];
FastPollingMutexSem incomingMutex;
TCPSConn **incoming;
uint32 numIncoming;
TCPSConn **conns;
uint32 numConns;
struct pollfd *pfds;
volatile int32 load;
};
} // namespace MARTe
#endif // TCPS_WORKER_H_
@@ -1,494 +0,0 @@
/**
* @file TCPStreamJoin.cpp
* @brief Source file for class TCPStreamJoin
* @date 06/11/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.
*/
#define DLL_API
#include "TCPStreamJoin.h"
#include "AdvancedErrorManagement.h"
#include "ConfigurationDatabase.h"
#include "ObjectRegistryDatabase.h"
#include "UDPSProtocol.h"
#include <stdio.h>
#include <string.h>
namespace MARTe {
const uint32 TCPStreamJoin::TCPSJ_MAX_SOURCES;
TCPStreamJoin::Source::Source()
: stream(NULL_PTR(TCPSServiceStream *)), configOk(false), cfg(NULL_PTR(uint8 *)),
cfgBytes(0u), numSigs(0u), hrt(0u), body(NULL_PTR(uint8 *)), bodyCap(0u),
bodySize(0u), haveBody(false) {
}
TCPStreamJoin::Source::~Source() {
if (cfg != NULL_PTR(uint8 *)) {
delete[] cfg;
}
if (body != NULL_PTR(uint8 *)) {
delete[] body;
}
}
TCPStreamJoin::TCPStreamJoin()
: Object(), TCPSStreamTapI(), TCPSStreamObserverI(), out(NULL_PTR(TCPSServiceStream *)),
observing(false), src(NULL_PTR(Source *)), numSrc(0u), master(0u), maxPayload(60000u),
active(false), configPublished(false), sizeLogged(false), outCounter(0u), emitted(0u),
scratch(NULL_PTR(uint8 *)), scratchCap(0u) {
}
TCPStreamJoin::~TCPStreamJoin() {
if (observing) {
service->RemoveObserver(this);
observing = false;
}
for (uint32 i = 0u; i < numSrc; i++) {
mtx.FastLock();
TCPSServiceStream *s = src[i].stream;
src[i].stream = NULL_PTR(TCPSServiceStream *);
mtx.FastUnLock();
if (s != NULL_PTR(TCPSServiceStream *)) {
s->RemoveTap(this);
}
}
if (out != NULL_PTR(TCPSServiceStream *)) {
delete out; // unregisters the merged stream
out = NULL_PTR(TCPSServiceStream *);
}
if (src != NULL_PTR(Source *)) {
delete[] src;
}
if (scratch != NULL_PTR(uint8 *)) {
delete[] scratch;
}
}
bool TCPStreamJoin::Initialise(StructuredDataI &data) {
bool ok = Object::Initialise(data);
StreamString serviceName;
if (ok && (!data.Read("Service", serviceName) || (serviceName.Size() == 0u))) {
REPORT_ERROR(ErrorManagement::ParametersError, "Parameter 'Service' is required.");
ok = false;
}
if (ok && (!data.Read("StreamName", outName) || (outName.Size() == 0u))) {
outName = GetName();
}
StreamString sources;
if (ok && (!data.Read("Sources", sources) || (sources.Size() == 0u))) {
REPORT_ERROR(ErrorManagement::ParametersError, "Parameter 'Sources' is required.");
ok = false;
}
StreamString masterName;
if (ok && !data.Read("Master", masterName)) {
masterName = "";
}
maxPayload = 60000u;
(void)data.Read("MaxPayloadSize", maxPayload);
if (ok) {
// Split "a, b ,c" into trimmed names.
src = new Source[TCPSJ_MAX_SOURCES];
const char8 *p = sources.Buffer();
while (ok && (p != NULL_PTR(const char8 *))) {
const char8 *comma = strchr(p, ',');
const char8 *end = (comma != NULL_PTR(const char8 *)) ? comma : (p + strlen(p));
while ((p < end) && (*p == ' ')) {
p++;
}
const char8 *e = end;
while ((e > p) && (e[-1] == ' ')) {
e--;
}
const uint32 len = static_cast<uint32>(e - p);
if ((len == 0u) || (len > TCPS_MAX_STREAM_NAME) || (numSrc >= TCPSJ_MAX_SOURCES)) {
REPORT_ERROR(ErrorManagement::ParametersError,
"Sources must list 1..%u names of 1..%u characters.",
TCPSJ_MAX_SOURCES, TCPS_MAX_STREAM_NAME);
ok = false;
}
if (ok) {
StreamString name;
uint32 wlen = len;
(void)name.Write(p, wlen);
for (uint32 i = 0u; i < numSrc; i++) {
if (src[i].name == name) {
REPORT_ERROR(ErrorManagement::ParametersError,
"Source '%s' is listed twice.", name.Buffer());
ok = false;
}
}
if (name == outName) {
REPORT_ERROR(ErrorManagement::ParametersError,
"The merged stream '%s' cannot be one of its own sources.",
name.Buffer());
ok = false;
}
src[numSrc].name = name;
numSrc++;
}
p = (comma != NULL_PTR(const char8 *)) ? (comma + 1) : NULL_PTR(const char8 *);
}
}
if (ok && (masterName.Size() > 0u)) {
const int32 m = FindByName(masterName.Buffer());
if (m < 0) {
REPORT_ERROR(ErrorManagement::ParametersError,
"Master '%s' is not one of the Sources.", masterName.Buffer());
ok = false;
}
else {
master = static_cast<uint32>(m);
}
}
if (ok) {
service = ObjectRegistryDatabase::Instance()->Find(serviceName.Buffer());
if (!service.IsValid()) {
REPORT_ERROR(ErrorManagement::InitialisationError,
"No TCPStreamService named '%s' (declare it before the join).",
serviceName.Buffer());
ok = false;
}
}
if (ok) {
out = new TCPSServiceStream(serviceName.Buffer(), outName.Buffer());
ConfigurationDatabase scdb;
ok = scdb.Write("MaxPayloadSize", maxPayload) && scdb.MoveToRoot() && out->Initialise(scdb);
}
if (ok) {
// Last: the replay of already registered streams may complete the join right here.
observing = service->AddObserver(this);
if (!observing) {
REPORT_ERROR(ErrorManagement::InitialisationError,
"The service cannot take another observer.");
ok = false;
}
}
return ok;
}
int32 TCPStreamJoin::FindByName(const char8 *name) const {
int32 r = -1;
for (uint32 i = 0u; (i < numSrc) && (r < 0); i++) {
if (strcmp(src[i].name.Buffer(), name) == 0) {
r = static_cast<int32>(i);
}
}
return r;
}
int32 TCPStreamJoin::FindByStream(const TCPSServiceStream *s) const {
int32 r = -1;
for (uint32 i = 0u; (i < numSrc) && (r < 0); i++) {
if (src[i].stream == s) {
r = static_cast<int32>(i);
}
}
return r;
}
bool TCPStreamJoin::AllRegistered() const {
bool all = true;
for (uint32 i = 0u; i < numSrc; i++) {
all = all && (src[i].stream != NULL_PTR(TCPSServiceStream *));
}
return all;
}
bool TCPStreamJoin::IsActive() const {
return active;
}
bool TCPStreamJoin::HasConfig() const {
return configPublished;
}
uint64 TCPStreamJoin::GetFramesEmitted() const {
return emitted;
}
uint32 TCPStreamJoin::GetNumSources() const {
return numSrc;
}
void TCPStreamJoin::OnStreamRegistered(TCPSServiceStream *stream) {
bool mine = false;
int32 idx = -1;
mtx.FastLock();
idx = FindByName(stream->GetStreamName());
if ((idx >= 0) && (src[idx].stream == NULL_PTR(TCPSServiceStream *))) {
src[idx].stream = stream;
mine = true;
}
mtx.FastUnLock();
if (!mine) {
return;
}
// Replays the cached CONFIG into OnTapConfig; the join mutex must not be held.
if (!stream->AddTap(this)) {
REPORT_ERROR(ErrorManagement::InitialisationError,
"Stream '%s' cannot take another tap.", stream->GetStreamName());
mtx.FastLock();
src[idx].stream = NULL_PTR(TCPSServiceStream *);
mtx.FastUnLock();
return;
}
Evaluate();
}
void TCPStreamJoin::Evaluate() {
mtx.FastLock();
const bool start = !active && AllRegistered();
mtx.FastUnLock();
if (start) {
// Registering may call back into observers (including this one): no lock held.
const bool ok = out->Start();
mtx.FastLock();
if (ok) {
active = true;
outCounter = 0u;
PublishConfigLocked();
}
mtx.FastUnLock();
}
}
void TCPStreamJoin::OnStreamUnregistered(TCPSServiceStream *stream) {
mtx.FastLock();
const int32 idx = FindByStream(stream);
bool wasActive = false;
if (idx >= 0) {
Source &s = src[idx];
s.stream = NULL_PTR(TCPSServiceStream *);
s.configOk = false;
s.haveBody = false;
s.bodySize = 0u;
wasActive = active;
active = false;
configPublished = false;
}
mtx.FastUnLock();
if (idx < 0) {
return;
}
stream->RemoveTap(this);
if (wasActive) {
(void)out->Stop(); // subscribers of the merged stream are dropped
}
}
bool TCPStreamJoin::ParseConfig(Source &s, const uint8 *payload, uint32 n) {
bool ok = (n >= 5u);
uint32 numSigs = 0u;
if (ok) {
memcpy(&numSigs, payload, 4u);
const uint64 need = 4u + static_cast<uint64>(numSigs) * UDPS_SIGNAL_DESC_SIZE + 1u;
ok = (numSigs > 0u) && (need <= n);
}
if (ok) {
const uint32 off = 4u + numSigs * UDPS_SIGNAL_DESC_SIZE;
if (payload[off] == UDPS_PUBLISH_ACCUMULATE) {
REPORT_ERROR(ErrorManagement::ParametersError,
"Source '%s' uses Accumulate publishing, which cannot be merged.",
s.name.Buffer());
ok = false;
}
else {
s.numSigs = numSigs;
s.hrt = 0u;
if (n >= (off + 1u + 8u)) {
memcpy(&s.hrt, &payload[off + 1u], 8u);
}
}
}
else {
REPORT_ERROR(ErrorManagement::ParametersError,
"Source '%s' sent a malformed CONFIG.", s.name.Buffer());
}
return ok;
}
void TCPStreamJoin::OnTapConfig(TCPSServiceStream *source, const uint8 *payload, uint32 payloadSize) {
mtx.FastLock();
const int32 idx = FindByStream(source);
if (idx >= 0) {
Source &s = src[idx];
s.haveBody = false;
s.bodySize = 0u;
configPublished = false;
sizeLogged = false;
s.configOk = ParseConfig(s, payload, payloadSize);
if (s.configOk) {
if (s.cfgBytes < payloadSize) {
if (s.cfg != NULL_PTR(uint8 *)) {
delete[] s.cfg;
}
s.cfg = new uint8[payloadSize];
}
memcpy(s.cfg, payload, payloadSize);
s.cfgBytes = payloadSize;
}
PublishConfigLocked();
}
mtx.FastUnLock();
}
/** Caller holds `mtx`. */
void TCPStreamJoin::PublishConfigLocked() {
configPublished = false;
bool ok = active;
uint64 totalSigs = 0u;
uint64 hrt = UDPS_HRT_FREQUENCY_UNKNOWN;
for (uint32 i = 0u; ok && (i < numSrc); i++) {
ok = src[i].configOk;
totalSigs += src[i].numSigs;
if (ok && (src[i].hrt != UDPS_HRT_FREQUENCY_UNKNOWN)) {
if ((hrt != UDPS_HRT_FREQUENCY_UNKNOWN) && (hrt != src[i].hrt)) {
REPORT_ERROR(ErrorManagement::ParametersError,
"Join '%s': sources have different HRT frequencies, cannot merge.",
outName.Buffer());
ok = false;
}
hrt = src[i].hrt;
}
}
const uint64 cfgSize = 4u + totalSigs * UDPS_SIGNAL_DESC_SIZE + 1u + 8u;
if (ok && (cfgSize > maxPayload)) {
REPORT_ERROR(ErrorManagement::ParametersError,
"Join '%s': merged CONFIG of %u bytes exceeds MaxPayloadSize.",
outName.Buffer(), static_cast<uint32>(cfgSize));
ok = false;
}
if (!ok) {
return;
}
uint8 *buf = new uint8[static_cast<uint32>(cfgSize)];
const uint32 nSigs = static_cast<uint32>(totalSigs);
memcpy(buf, &nSigs, 4u);
uint32 pos = 4u;
uint32 base = 0u;
for (uint32 i = 0u; ok && (i < numSrc); i++) {
const char8 *prefix = src[i].name.Buffer();
for (uint32 k = 0u; ok && (k < src[i].numSigs); k++) {
UDPSSignalDescriptor d;
memcpy(&d, &src[i].cfg[4u + k * UDPS_SIGNAL_DESC_SIZE], UDPS_SIGNAL_DESC_SIZE);
d.name[UDPS_MAX_SIGNAL_NAME - 1u] = '\0';
const uint32 len = static_cast<uint32>(strlen(prefix) + 1u + strlen(d.name));
if (len >= UDPS_MAX_SIGNAL_NAME) {
REPORT_ERROR(ErrorManagement::ParametersError,
"Join '%s': signal name '%s_%s' is longer than %u characters.",
outName.Buffer(), prefix, d.name, UDPS_MAX_SIGNAL_NAME - 1u);
ok = false;
}
else {
char8 merged[UDPS_MAX_SIGNAL_NAME];
(void)snprintf(merged, sizeof(merged), "%s_%s", prefix, d.name);
memset(d.name, 0, sizeof(d.name));
memcpy(d.name, merged, strlen(merged));
if (d.timeSignalIdx != UDPS_NO_TIME_SIGNAL) {
d.timeSignalIdx += base;
}
memcpy(&buf[pos], &d, UDPS_SIGNAL_DESC_SIZE);
pos += UDPS_SIGNAL_DESC_SIZE;
}
}
base += src[i].numSigs;
}
if (ok) {
buf[pos] = UDPS_PUBLISH_STRICT;
pos += 1u;
memcpy(&buf[pos], &hrt, 8u);
pos += 8u;
ok = out->SendConfig(buf, pos);
}
configPublished = ok;
delete[] buf;
}
void TCPStreamJoin::OnTapData(TCPSServiceStream *source, uint32 counter, const uint8 *payload,
uint32 payloadSize) {
(void)counter;
mtx.FastLock();
const int32 idx = FindByStream(source);
if ((idx >= 0) && src[idx].configOk && (payloadSize >= 8u)) {
Source &s = src[idx];
const uint32 bl = payloadSize - 8u;
// A size change without a new CONFIG would corrupt the merged layout: ignore.
if (!s.haveBody || (s.bodySize == bl)) {
if (s.bodyCap < bl) {
if (s.body != NULL_PTR(uint8 *)) {
delete[] s.body;
}
s.body = new uint8[(bl > 0u) ? bl : 1u];
s.bodyCap = (bl > 0u) ? bl : 1u;
}
if (bl > 0u) {
memcpy(s.body, &payload[8], bl);
}
s.bodySize = bl;
s.haveBody = true;
if (active && configPublished && (static_cast<uint32>(idx) == master)) {
uint64 total = 8u;
bool ready = true;
for (uint32 i = 0u; i < numSrc; i++) {
ready = ready && src[i].haveBody;
total += src[i].bodySize;
}
if (ready && (total > maxPayload)) {
if (!sizeLogged) {
REPORT_ERROR(ErrorManagement::ParametersError,
"Join '%s': merged frame of %u bytes exceeds MaxPayloadSize.",
outName.Buffer(), static_cast<uint32>(total));
sizeLogged = true;
}
ready = false;
}
if (ready) {
if (scratchCap < total) {
if (scratch != NULL_PTR(uint8 *)) {
delete[] scratch;
}
scratch = new uint8[static_cast<uint32>(total)];
scratchCap = static_cast<uint32>(total);
}
memcpy(scratch, payload, 8u); // master timestamp
uint32 pos = 8u;
for (uint32 i = 0u; i < numSrc; i++) {
if (src[i].bodySize > 0u) {
memcpy(&scratch[pos], src[i].body, src[i].bodySize);
}
pos += src[i].bodySize;
}
outCounter++;
if (out->SendData(outCounter, scratch, pos)) {
emitted = emitted + 1u;
}
}
}
}
}
mtx.FastUnLock();
}
CLASS_REGISTER(TCPStreamJoin, "1.0")
} /* namespace MARTe */
@@ -1,139 +0,0 @@
#ifndef TCPSTREAMJOIN_H_
#define TCPSTREAMJOIN_H_
/**
* @file TCPStreamJoin.h
* @brief Publishes one new stream of a TCPStreamService that merges several other
* streams of the same service.
*
* @code
* +Svc = { Class = TCPStreamService Port = 44500 }
* +Join = { Class = TCPStreamJoin Service = "Svc" StreamName = "all"
* Sources = "fast,slow" Master = "fast" }
* @endcode
* Declare the join after the service. Consumers subscribe to "all" like to any stream.
*
* The merged stream is emitted at the pace of the Master source: each Master DATA frame
* produces one merged frame holding the Master's signals and the last values received
* from every other source (sample-and-hold). Nothing is emitted until every source has
* delivered one DATA frame. No thread is added: the merged frame is built and queued on
* the Master producer's thread.
*
* @par CONFIG
* Concatenation of the source descriptors in Sources order, named
* "<stream>_<signal>" (at most 63 characters), time-signal indexes rebased, publish mode
* Strict. All sources must share the same HRT frequency (an unknown one matches any).
*
* @par DATA
* [master timestamp][source0 body][source1 body]... where a body is that source's last
* DATA payload without its 8-byte timestamp. A held value is therefore stamped with the
* Master's time unless the source carries its own time signal.
*
* @par Limits
* Only Strict and Decimate sources are accepted (Accumulate batches have varying
* length); a source whose frame size changes without a new CONFIG is ignored; the merged
* frame must fit MaxPayloadSize. The output stream is registered while every source is
* registered and removed as soon as one leaves; it is rebuilt when a source re-registers
* or changes its CONFIG.
*
* @par Parameters
* | Parameter | Type | Default | Description |
* |----------------|--------|-------------|-------------|
* | Service | string | (required) | Name of the TCPStreamService object. |
* | StreamName | string | object name | Name of the merged stream. |
* | Sources | string | (required) | Comma separated source stream names (1..16). |
* | Master | string | first source| Source whose frames pace the merged stream. |
* | MaxPayloadSize | uint32 | 60000 | Largest merged frame payload. |
*
* @par Locking
* Producer-side: source tapMutex, then the join mutex, then the output stream mutex, then
* the connection queue mutex. Service notifications run outside its topology lock and
* are serialised.
*/
#include "FastPollingMutexSem.h"
#include "Object.h"
#include "ReferenceT.h"
#include "StreamString.h"
#include "StructuredDataI.h"
#include "TCPSServiceStream.h"
#include "TCPSStreamTapI.h"
#include "TCPStreamService.h"
namespace MARTe {
class TCPStreamJoin : public Object, public TCPSStreamTapI, public TCPSStreamObserverI {
public:
CLASS_REGISTER_DECLARATION()
/** Most source streams one join can merge. */
static const uint32 TCPSJ_MAX_SOURCES = 16u;
TCPStreamJoin();
virtual ~TCPStreamJoin();
virtual bool Initialise(StructuredDataI &data);
virtual void OnTapConfig(TCPSServiceStream *source, const uint8 *payload, uint32 payloadSize);
virtual void OnTapData(TCPSServiceStream *source, uint32 counter, const uint8 *payload,
uint32 payloadSize);
virtual void OnStreamRegistered(TCPSServiceStream *stream);
virtual void OnStreamUnregistered(TCPSServiceStream *stream);
/** @return true while the merged stream is registered with the service. */
bool IsActive() const;
/** @return true once a merged CONFIG has been published for the current sources. */
bool HasConfig() const;
/** @return merged DATA frames emitted. */
uint64 GetFramesEmitted() const;
uint32 GetNumSources() const;
private:
struct Source {
Source();
~Source();
StreamString name;
TCPSServiceStream *stream; ///< non-NULL while registered and tapped
bool configOk;
uint8 *cfg;
uint32 cfgBytes;
uint32 numSigs;
uint64 hrt;
uint8 *body;
uint32 bodyCap;
uint32 bodySize;
bool haveBody;
};
int32 FindByName(const char8 *name) const;
int32 FindByStream(const TCPSServiceStream *s) const;
bool AllRegistered() const;
void Evaluate();
void PublishConfigLocked();
bool ParseConfig(Source &s, const uint8 *payload, uint32 n);
ReferenceT<TCPStreamService> service;
TCPSServiceStream *out;
bool observing;
StreamString outName;
Source *src;
uint32 numSrc;
uint32 master;
uint32 maxPayload;
FastPollingMutexSem mtx;
bool active;
bool configPublished;
bool sizeLogged;
uint32 outCounter;
volatile uint64 emitted;
uint8 *scratch;
uint32 scratchCap;
TCPStreamJoin(const TCPStreamJoin &);
TCPStreamJoin &operator=(const TCPStreamJoin &);
};
} // namespace MARTe
#endif // TCPSTREAMJOIN_H_
@@ -1,502 +0,0 @@
/**
* @file TCPStreamService.cpp
* @brief Source file for class TCPStreamService
* @date 06/11/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.
*/
#define DLL_API
#include "TCPStreamService.h"
#include "AdvancedErrorManagement.h"
#include "Atomic.h"
#include "HighResolutionTimer.h"
#include "TCPSConn.h"
#include "TCPSServiceStream.h"
#include "TCPSWorker.h"
#include <arpa/inet.h>
#include <errno.h>
#include <fcntl.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <poll.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
namespace MARTe {
static const int TCPS_ACCEPT_SLICE_MS = 50;
TCPStreamService::TCPStreamService()
: Object(), EmbeddedServiceMethodBinderI(), port(44500u), boundPort(0u),
numWorkers(2u), maxClients(64u), maxStreams(32u), queueDepth(8u),
stallTimeoutS(5u), handshakeTimeoutS(5u), sendBufferSize(0u),
cpuMask(0xFFFFFFFFu), stackSize(65536u), stallTicks(0u), handshakeTicks(0u),
listenFd(-1), acceptThread(*this), acceptorStarted(false),
workers(NULL_PTR(TCPSWorker **)), workersCreated(0u),
numObservers(0u),
streams(NULL_PTR(TCPSServiceStream **)), numStreams(0u),
connCount(0), evictions(0), rejected(0) {
(void)notifyMutex.Create(true);
for (uint32 i = 0u; i < TCPS_MAX_OBSERVERS; i++) {
observers[i] = NULL_PTR(TCPSStreamObserverI *);
}
}
TCPStreamService::~TCPStreamService() {
Shutdown();
}
void TCPStreamService::Shutdown() {
if (acceptorStarted) {
(void)acceptThread.Stop();
acceptorStarted = false;
}
for (uint32 i = 0u; i < workersCreated; i++) {
workers[i]->Stop();
}
if (listenFd >= 0) {
(void)close(listenFd);
listenFd = -1;
}
for (uint32 i = 0u; i < workersCreated; i++) {
delete workers[i];
}
workersCreated = 0u;
if (workers != NULL_PTR(TCPSWorker **)) {
delete[] workers;
workers = NULL_PTR(TCPSWorker **);
}
if (streams != NULL_PTR(TCPSServiceStream **)) {
delete[] streams;
streams = NULL_PTR(TCPSServiceStream **);
}
numStreams = 0u;
}
bool TCPStreamService::Initialise(StructuredDataI &data) {
bool ok = Object::Initialise(data);
port = 44500u;
(void)data.Read("Port", port);
if (port > 65535u) {
REPORT_ERROR(ErrorManagement::ParametersError, "Port %u out of range.", port);
ok = false;
}
numWorkers = 2u;
(void)data.Read("Workers", numWorkers);
if ((numWorkers == 0u) || (numWorkers > 32u)) {
REPORT_ERROR(ErrorManagement::ParametersError, "Workers %u must be in 1..32.", numWorkers);
ok = false;
}
maxClients = 64u;
(void)data.Read("MaxClients", maxClients);
if ((maxClients == 0u) || (maxClients > 1024u)) {
REPORT_ERROR(ErrorManagement::ParametersError, "MaxClients %u must be in 1..1024.", maxClients);
ok = false;
}
maxStreams = 32u;
(void)data.Read("MaxStreams", maxStreams);
if ((maxStreams == 0u) || (maxStreams > 256u)) {
REPORT_ERROR(ErrorManagement::ParametersError, "MaxStreams %u must be in 1..256.", maxStreams);
ok = false;
}
queueDepth = 8u;
(void)data.Read("ClientQueueDepth", queueDepth);
if ((queueDepth == 0u) || (queueDepth > 4096u)) {
REPORT_ERROR(ErrorManagement::ParametersError, "ClientQueueDepth %u must be in 1..4096.", queueDepth);
ok = false;
}
stallTimeoutS = 5u;
(void)data.Read("ClientStallTimeout", stallTimeoutS);
handshakeTimeoutS = 5u;
(void)data.Read("HandshakeTimeout", handshakeTimeoutS);
if (handshakeTimeoutS == 0u) {
REPORT_ERROR(ErrorManagement::ParametersError, "HandshakeTimeout must be at least 1 second.");
ok = false;
}
sendBufferSize = 0u;
(void)data.Read("SendBufferSize", sendBufferSize);
(void)data.Read("CPUMask", cpuMask);
(void)data.Read("StackSize", stackSize);
stallTicks = static_cast<uint64>(stallTimeoutS) * HighResolutionTimer::Frequency();
handshakeTicks = static_cast<uint64>(handshakeTimeoutS) * HighResolutionTimer::Frequency();
if (!ok) {
return false;
}
/* Listening socket. */
int fd = socket(AF_INET, SOCK_STREAM, 0);
ok = (fd >= 0);
if (!ok) {
REPORT_ERROR(ErrorManagement::FatalError, "socket() failed: %s.", strerror(errno));
}
if (ok) {
int one = 1;
(void)setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr.sin_port = htons(static_cast<uint16>(port));
if (bind(fd, reinterpret_cast<struct sockaddr *>(&addr), sizeof(addr)) != 0) {
REPORT_ERROR(ErrorManagement::FatalError, "bind(%u) failed: %s.", port, strerror(errno));
ok = false;
}
}
if (ok && (listen(fd, 32) != 0)) {
REPORT_ERROR(ErrorManagement::FatalError, "listen() failed: %s.", strerror(errno));
ok = false;
}
if (ok) {
int flags = fcntl(fd, F_GETFL, 0);
ok = (flags >= 0) && (fcntl(fd, F_SETFL, flags | O_NONBLOCK) == 0);
}
if (ok) {
struct sockaddr_in addr;
socklen_t len = static_cast<socklen_t>(sizeof(addr));
memset(&addr, 0, sizeof(addr));
boundPort = static_cast<uint16>(port);
if (getsockname(fd, reinterpret_cast<struct sockaddr *>(&addr), &len) == 0) {
boundPort = ntohs(addr.sin_port);
}
}
if (!ok) {
if (fd >= 0) {
(void)close(fd);
}
return false;
}
listenFd = fd;
streams = new TCPSServiceStream *[maxStreams];
workers = new TCPSWorker *[numWorkers];
for (uint32 i = 0u; (i < numWorkers) && ok; i++) {
workers[i] = new TCPSWorker(*this, maxClients);
workersCreated = i + 1u;
ok = workers[i]->Start(cpuMask, stackSize);
if (!ok) {
REPORT_ERROR(ErrorManagement::FatalError, "Could not start worker %u.", i);
}
}
if (ok) {
acceptThread.SetCPUMask(cpuMask);
acceptThread.SetStackSize(stackSize);
ok = (acceptThread.Start() == ErrorManagement::NoError);
acceptorStarted = ok;
if (!ok) {
REPORT_ERROR(ErrorManagement::FatalError, "Could not start the acceptor thread.");
}
}
if (!ok) {
Shutdown();
}
return ok;
}
ErrorManagement::ErrorType TCPStreamService::Execute(ExecutionInfo &info) {
if (info.GetStage() != ExecutionInfo::MainStage) {
return ErrorManagement::NoError;
}
struct pollfd p;
p.fd = listenFd;
p.events = POLLIN;
p.revents = 0;
if (poll(&p, 1, TCPS_ACCEPT_SLICE_MS) <= 0) {
return ErrorManagement::NoError;
}
for (;;) {
int fd = accept(listenFd, NULL_PTR(struct sockaddr *), NULL_PTR(socklen_t *));
if (fd < 0) {
break;
}
if (static_cast<uint32>(connCount) >= maxClients) {
REPORT_ERROR(ErrorManagement::Warning,
"MaxClients (%u) reached; connection refused.", maxClients);
(void)close(fd);
continue;
}
int flags = fcntl(fd, F_GETFL, 0);
if ((flags < 0) || (fcntl(fd, F_SETFL, flags | O_NONBLOCK) != 0)) {
(void)close(fd);
continue;
}
int one = 1;
(void)setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &one, sizeof(one));
if (sendBufferSize > 0u) {
int sz = static_cast<int>(sendBufferSize);
(void)setsockopt(fd, SOL_SOCKET, SO_SNDBUF, &sz, sizeof(sz));
}
TCPSWorker *best = workers[0];
for (uint32 i = 1u; i < workersCreated; i++) {
if (workers[i]->Load() < best->Load()) {
best = workers[i];
}
}
TCPSConn *c = new TCPSConn(fd, best, queueDepth,
HighResolutionTimer::Counter() + handshakeTicks);
Atomic::Increment(&connCount);
if (!best->AddIncoming(c)) {
delete c;
Atomic::Decrement(&connCount);
}
}
return ErrorManagement::NoError;
}
uint16 TCPStreamService::GetBoundPort() const {
return boundPort;
}
uint32 TCPStreamService::GetClientCount() const {
uint32 n = 0u;
topology.FastLock();
for (uint32 i = 0u; i < numStreams; i++) {
n += streams[i]->GetClientCount();
}
topology.FastUnLock();
return n;
}
uint32 TCPStreamService::GetStreamCount() const {
return numStreams;
}
uint32 TCPStreamService::GetWorkerCount() const {
return workersCreated;
}
uint64 TCPStreamService::GetClientDrops() const {
uint64 n = 0u;
topology.FastLock();
for (uint32 i = 0u; i < numStreams; i++) {
n += streams[i]->GetClientDrops();
}
topology.FastUnLock();
return n;
}
uint32 TCPStreamService::GetStallEvictions() const {
return static_cast<uint32>(evictions);
}
uint32 TCPStreamService::GetRejectedSubscriptions() const {
return static_cast<uint32>(rejected);
}
bool TCPStreamService::RegisterStream(TCPSServiceStream *s) {
bool ok = true;
// Held across the registration so observers see (un)registrations in order.
(void)notifyMutex.Lock();
topology.FastLock();
if (numStreams >= maxStreams) {
REPORT_ERROR(ErrorManagement::ParametersError,
"Cannot register stream '%s': MaxStreams (%u) reached.",
s->GetStreamName(), maxStreams);
ok = false;
}
for (uint32 i = 0u; (i < numStreams) && ok; i++) {
if (strcmp(streams[i]->GetStreamName(), s->GetStreamName()) == 0) {
REPORT_ERROR(ErrorManagement::ParametersError,
"A stream named '%s' is already registered.", s->GetStreamName());
ok = false;
}
}
if (ok) {
streams[numStreams] = s;
numStreams++;
}
topology.FastUnLock();
if (ok) {
for (uint32 i = 0u; i < numObservers; i++) {
observers[i]->OnStreamRegistered(s);
}
}
(void)notifyMutex.UnLock();
return ok;
}
void TCPStreamService::UnregisterStream(TCPSServiceStream *s) {
(void)notifyMutex.Lock();
bool wasRegistered = false;
topology.FastLock();
for (uint32 i = 0u; i < numStreams; i++) {
if (streams[i] == s) {
streams[i] = streams[numStreams - 1u];
numStreams--;
wasRegistered = true;
break;
}
}
s->DropAllConns();
topology.FastUnLock();
if (wasRegistered) {
for (uint32 i = 0u; i < numObservers; i++) {
observers[i]->OnStreamUnregistered(s);
}
}
(void)notifyMutex.UnLock();
}
bool TCPStreamService::AddObserver(TCPSStreamObserverI *obs) {
bool ok = false;
(void)notifyMutex.Lock();
if (numObservers < TCPS_MAX_OBSERVERS) {
observers[numObservers] = obs;
numObservers++;
ok = true;
// Replay: streams cannot (un)register while notifyMutex is held.
topology.FastLock();
const uint32 n = numStreams;
TCPSServiceStream **snap = new TCPSServiceStream *[(n > 0u) ? n : 1u];
for (uint32 i = 0u; i < n; i++) {
snap[i] = streams[i];
}
topology.FastUnLock();
for (uint32 i = 0u; i < n; i++) {
obs->OnStreamRegistered(snap[i]);
}
delete[] snap;
}
(void)notifyMutex.UnLock();
return ok;
}
void TCPStreamService::RemoveObserver(TCPSStreamObserverI *obs) {
(void)notifyMutex.Lock();
for (uint32 i = 0u; i < numObservers; i++) {
if (observers[i] == obs) {
observers[i] = observers[numObservers - 1u];
numObservers--;
break;
}
}
(void)notifyMutex.UnLock();
}
bool TCPStreamService::Attach(TCPSConn *c, char8 *streamList) {
TCPSServiceStream *found[TCPS_MAX_CONN_STREAMS];
uint32 n = 0u;
bool ok = true;
topology.FastLock();
char8 *p = streamList;
while (ok) {
char8 *comma = strchr(p, ',');
if (comma != NULL_PTR(char8 *)) {
*comma = '\0';
}
const uint32 len = static_cast<uint32>(strlen(p));
TCPSServiceStream *match = NULL_PTR(TCPSServiceStream *);
if ((len == 0u) || (len > TCPS_MAX_STREAM_NAME) || (n >= TCPS_MAX_CONN_STREAMS)) {
ok = false;
}
for (uint32 i = 0u; ok && (i < numStreams) && (match == NULL_PTR(TCPSServiceStream *)); i++) {
if (strcmp(streams[i]->GetStreamName(), p) == 0) {
match = streams[i];
}
}
for (uint32 i = 0u; ok && (i < n); i++) {
if (found[i] == match) {
ok = false; // the same stream listed twice
}
}
if (ok && (match == NULL_PTR(TCPSServiceStream *))) {
ok = false;
}
if (ok) {
found[n] = match;
n++;
if (comma == NULL_PTR(char8 *)) {
break;
}
p = comma + 1;
}
}
if (ok) {
c->qMutex.FastLock();
c->q.Reserve(n);
c->qMutex.FastUnLock();
uint32 attached = 0u;
for (uint32 i = 0u; ok && (i < n); i++) {
ok = found[i]->AttachConn(c, static_cast<uint16>(i));
if (ok) {
attached++;
}
}
if (!ok) {
for (uint32 i = 0u; i < attached; i++) {
found[i]->DetachConn(c);
}
} else {
for (uint32 i = 0u; i < n; i++) {
c->streams[i] = found[i];
}
c->numStreams = n;
}
}
topology.FastUnLock();
return ok;
}
void TCPStreamService::Detach(TCPSConn *c) {
topology.FastLock();
for (uint32 i = 0u; i < c->numStreams; i++) {
if (c->streams[i] != NULL_PTR(TCPSServiceStream *)) {
c->streams[i]->DetachConn(c);
c->streams[i] = NULL_PTR(TCPSServiceStream *);
}
}
topology.FastUnLock();
}
void TCPStreamService::ConnClosed() {
Atomic::Decrement(&connCount);
}
void TCPStreamService::NoteEviction() {
Atomic::Increment(&evictions);
}
void TCPStreamService::NoteRejected() {
Atomic::Increment(&rejected);
}
uint32 TCPStreamService::QueueDepth() const {
return queueDepth;
}
uint64 TCPStreamService::StallTicks() const {
return stallTicks;
}
uint64 TCPStreamService::HandshakeTicks() const {
return handshakeTicks;
}
uint32 TCPStreamService::MaxClients() const {
return maxClients;
}
CLASS_REGISTER(TCPStreamService, "1.0")
} // namespace MARTe
@@ -1,174 +0,0 @@
#ifndef TCPSTREAMSERVICE_H_
#define TCPSTREAMSERVICE_H_
/**
* @file TCPStreamService.h
* @brief One in-process TCP streaming server shared by any number of
* TCPStreamer DataSources, served by a pool of worker threads.
*
* @details Declared once, at the root of the configuration:
* <pre>
* +StreamService = {
* Class = TCPStreamService
* Port = 44500
* Workers = 2
* }
* </pre>
* and referenced from any number of DataSources:
* <pre>
* +Fast = { Class = TCPStreamer Service = StreamService StreamName = Fast ... }
* +Slow = { Class = TCPStreamer Service = StreamService StreamName = Slow ... }
* </pre>
*
* Wire protocol: plain UDPS framing over TCP, exactly like TCPSServer, plus one
* client-to-server frame. Right after connecting, a client sends a SUBSCRIBE
* frame (UDPS_TYPE_SUBSCRIBE, payload = stream name, 1..255 bytes). The service
* then sends that stream's CONFIG followed by its DATA frames. One connection =
* one stream. A connection that does not subscribe within HandshakeTimeout
* seconds, or that names an unknown stream, is closed (clients retry).
*
* Threads: one acceptor plus `Workers` I/O threads. A connection lives on one
* worker. Producer threads never touch sockets: they copy each frame into every
* subscriber's bounded queue and poke the owning worker through a self-pipe, so
* a slow client only loses its own frames and never delays a producer or
* another client.
*
* @par Parameters (all optional)
* | Parameter | Type | Default | Description |
* |--------------------|--------|---------|-------------|
* | Port | uint32 | 44500 | TCP listen port; 0 = ephemeral (see GetBoundPort()). |
* | Workers | uint32 | 2 | I/O threads (1..32). |
* | MaxClients | uint32 | 64 | Simultaneous connections (1..1024). |
* | MaxStreams | uint32 | 32 | Registered streams (1..256). |
* | ClientQueueDepth | uint32 | 8 | DATA frames queued per client (1..4096). |
* | ClientStallTimeout | uint32 | 5 | Seconds without progress before eviction; 0 disables. |
* | HandshakeTimeout | uint32 | 5 | Seconds allowed to send SUBSCRIBE. |
* | SendBufferSize | uint32 | 0 | SO_SNDBUF for client sockets; 0 keeps the OS default. |
* | CPUMask | uint32 | 0xFFFFFFFF | Affinity of the service threads. |
* | StackSize | uint32 | 65536 | Stack of the service threads. |
*/
#include "EmbeddedServiceMethodBinderI.h"
#include "ExecutionInfo.h"
#include "FastPollingMutexSem.h"
#include "MutexSem.h"
#include "Object.h"
#include "SingleThreadService.h"
#include "StructuredDataI.h"
#include "TCPSStreamTapI.h"
#include "UDPSProtocol.h"
namespace MARTe {
class TCPSWorker;
class TCPSServiceStream;
struct TCPSConn;
/** Longest stream name accepted in a SUBSCRIBE frame. */
static const uint32 TCPS_MAX_STREAM_NAME = 255u;
/** Most streams one connection may multiplex. */
static const uint32 TCPS_MAX_CONN_STREAMS = UDPS_TCP_MAX_STREAMS;
/** Longest SUBSCRIBE payload: a comma separated list of stream names. */
static const uint32 TCPS_MAX_SUBSCRIBE_PAYLOAD = UDPS_TCP_MAX_SUBSCRIBE_PAYLOAD;
class TCPStreamService : public Object, public EmbeddedServiceMethodBinderI {
public:
CLASS_REGISTER_DECLARATION()
TCPStreamService();
virtual ~TCPStreamService();
/** Reads the parameters, opens the listening socket and starts the threads. */
virtual bool Initialise(StructuredDataI &data);
/** Acceptor thread entry point; do not call directly. */
virtual ErrorManagement::ErrorType Execute(ExecutionInfo &info);
/** @return the port actually bound. */
uint16 GetBoundPort() const;
/** @return subscribed connections across all streams. */
uint32 GetClientCount() const;
uint32 GetStreamCount() const;
uint32 GetWorkerCount() const;
/** @return DATA frames dropped for slow clients, summed over registered streams. */
uint64 GetClientDrops() const;
/** @return connections closed by the stall timeout (all time). */
uint32 GetStallEvictions() const;
/** @return connections closed for a bad / unknown SUBSCRIBE (all time). */
uint32 GetRejectedSubscriptions() const;
/* ---- stream side ---- */
/** @return false if a stream with this name is already registered or the table is full. */
bool RegisterStream(TCPSServiceStream *s);
/** Remove the stream and make its subscribers close. */
void UnregisterStream(TCPSServiceStream *s);
/**
* Be told when streams register / unregister. The streams already registered are
* replayed to the observer before AddObserver returns. Notifications are serialised
* (and may nest on one thread), they never run under the topology lock, and once
* RemoveObserver returns no callback is running or will start.
* @return false if the observer table is full.
*/
bool AddObserver(TCPSStreamObserverI *obs);
void RemoveObserver(TCPSStreamObserverI *obs);
/* ---- worker side (internal) ---- */
/**
* Bind a connection to the streams named in @p streamList (comma separated,
* modified in place). The position in the list is the stream index stamped
* on every frame. All-or-nothing: @return false if any name is unknown,
* duplicated or malformed.
*/
bool Attach(TCPSConn *c, char8 *streamList);
void Detach(TCPSConn *c);
void ConnClosed();
void NoteEviction();
void NoteRejected();
uint32 QueueDepth() const;
uint64 StallTicks() const;
uint64 HandshakeTicks() const;
/** Max subscribers a stream may hold. */
uint32 MaxClients() const;
private:
void Shutdown();
uint32 port;
uint16 boundPort;
uint32 numWorkers;
uint32 maxClients;
uint32 maxStreams;
uint32 queueDepth;
uint32 stallTimeoutS;
uint32 handshakeTimeoutS;
uint32 sendBufferSize;
uint32 cpuMask;
uint32 stackSize;
uint64 stallTicks;
uint64 handshakeTicks;
int listenFd;
SingleThreadService acceptThread;
bool acceptorStarted;
TCPSWorker **workers;
uint32 workersCreated;
/** Most observers (TCPStreamJoin objects) one service can feed. */
static const uint32 TCPS_MAX_OBSERVERS = 16u;
MutexSem notifyMutex; ///< recursive; serialises observer calls with (un)registration
TCPSStreamObserverI *observers[TCPS_MAX_OBSERVERS];
uint32 numObservers;
mutable FastPollingMutexSem topology; ///< guards streams[] and every TCPSConn::stream
TCPSServiceStream **streams;
uint32 numStreams;
volatile int32 connCount;
volatile int32 evictions;
volatile int32 rejected;
};
} // namespace MARTe
#endif // TCPSTREAMSERVICE_H_
@@ -1,353 +0,0 @@
../../../..//Build/x86-linux/Components/Interfaces/TCPStreamService/TCPSFrameQueue.o: TCPSFrameQueue.cpp TCPSFrameQueue.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/CompilerTypes.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/Architecture/x86_gcc/CompilerTypes.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/HighResolutionTimer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/GeneralDefinitions.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/TypeCharacteristics.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/CompilerTypes.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/GeneralDefinitions.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/TimeStamp.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Architecture/x86_gcc/HighResolutionTimerA.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/HighResolutionTimer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/../../HighResolutionTimer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/HighResolutionTimerCalibrator.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/../../GeneralDefinitions.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/TimeStamp.h
../../../..//Build/x86-linux/Components/Interfaces/TCPStreamService/TCPSServiceStream.o: TCPSServiceStream.cpp TCPSServiceStream.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/FastPollingMutexSem.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Atomic.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/GeneralDefinitions.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/TypeCharacteristics.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/CompilerTypes.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/Architecture/x86_gcc/CompilerTypes.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/GeneralDefinitions.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Architecture/x86_gcc/AtomicA.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/CompilerTypes.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/ErrorManagement.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/ErrorInformation.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/ErrorType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BitBoolean.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/TemplateParametersVerificator.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BitRange.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/TypeCharacteristics.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StreamI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/TimeoutType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/HighResolutionTimer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/TimeStamp.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Architecture/x86_gcc/HighResolutionTimerA.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/HighResolutionTimer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/../../HighResolutionTimer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/HighResolutionTimerCalibrator.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/../../GeneralDefinitions.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/TimeStamp.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Sleep.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ReferenceT.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/Reference.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/Object.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/HeapI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/HeapManager.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/HeapI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ClassProperties.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/GeneralDefinitions.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/FractionalInteger.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ClassRegistryItem.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/CString.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/ZeroTerminatedArray.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/Introspection.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/IntrospectionEntry.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/TypeDescriptor.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BasicType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/FractionalInteger.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BitRange.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BitBoolean.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/ZeroTerminatedArray.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/LinkedListable.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/LinkedListHolderT.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/IteratorT.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/Iterator.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/LinkedListable.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/LinkedListHolder.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/SortFilter.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/SearchFilter.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/LoadableLibrary.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ObjectBuilder.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StringHelper.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/StructuredDataI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/AnyType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ClassRegistryDatabase.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/GlobalObjectsDatabase.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/GlobalObjectI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/StandardHeap.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/../../HeapI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/../Generic/StandardHeap_Generic.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StaticList.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StaticListHolder.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/Matrix.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/Vector.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/MemoryOperationsHelper.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/CLASSREGISTER.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ClassRegistryItemT.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ObjectBuilderT.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/StreamString.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/CharBuffer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/ErrorManagement.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/BufferedStreamI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/TimeoutType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/AnyType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/FormatDescriptor.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/IOBuffer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StreamI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/StreamStringIOBuffer.h \
TCPStreamService.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/EmbeddedServiceMethodBinderI.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/ExecutionInfo.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L1Portability/Threads.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/ThreadsB.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L1Portability/ExceptionHandler.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/ProcessorType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BitSet.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/ExecutionInfo.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/Object.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/SingleThreadService.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/EmbeddedServiceI.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/EmbeddedServiceMethodBinderI.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/EmbeddedServiceMethodBinderT.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/EmbeddedThreadI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ReferenceContainer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/LinkedListHolder.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ReferenceContainerFilter.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ReferenceContainerNode.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/StructuredDataI.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/EmbeddedThread.h \
../../../..//Common/UDP/UDPSProtocol.h \
../../../..//Source/Components/Interfaces/UDPStream/UDPSTransportI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/AdvancedErrorManagement.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ClassProperties.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/StreamMemoryReference.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ObjectRegistryDatabase.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ReferenceContainer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ReferenceT.h \
TCPSConn.h TCPSFrameQueue.h TCPSWorker.h
../../../..//Build/x86-linux/Components/Interfaces/TCPStreamService/TCPStreamService.o: TCPStreamService.cpp TCPStreamService.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/EmbeddedServiceMethodBinderI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/ErrorType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/CompilerTypes.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/Architecture/x86_gcc/CompilerTypes.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BitBoolean.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/TemplateParametersVerificator.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BitRange.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/TypeCharacteristics.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/ExecutionInfo.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BitRange.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BitBoolean.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L1Portability/Threads.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/ThreadsB.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/GeneralDefinitions.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/TypeCharacteristics.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/GeneralDefinitions.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/GeneralDefinitions.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L1Portability/ExceptionHandler.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/CompilerTypes.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/ProcessorType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BitSet.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/ExecutionInfo.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/FastPollingMutexSem.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Atomic.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Architecture/x86_gcc/AtomicA.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/ErrorManagement.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/ErrorInformation.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StreamI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/TimeoutType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/HighResolutionTimer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/TimeStamp.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Architecture/x86_gcc/HighResolutionTimerA.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/HighResolutionTimer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/../../HighResolutionTimer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/HighResolutionTimerCalibrator.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/../../GeneralDefinitions.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/TimeStamp.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Sleep.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/Object.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/HeapI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/HeapManager.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/HeapI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ClassProperties.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/FractionalInteger.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ClassRegistryItem.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/CString.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/ZeroTerminatedArray.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/Introspection.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/IntrospectionEntry.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/TypeDescriptor.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BasicType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/FractionalInteger.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/ZeroTerminatedArray.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/LinkedListable.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/LinkedListHolderT.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/IteratorT.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/Iterator.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/LinkedListable.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/LinkedListHolder.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/SortFilter.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/SearchFilter.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/LoadableLibrary.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ObjectBuilder.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StringHelper.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/StructuredDataI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/AnyType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ClassRegistryDatabase.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/GlobalObjectsDatabase.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/GlobalObjectI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/StandardHeap.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/../../HeapI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/../Generic/StandardHeap_Generic.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StaticList.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StaticListHolder.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/Matrix.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/Vector.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/MemoryOperationsHelper.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/CLASSREGISTER.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ClassRegistryItemT.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ObjectBuilderT.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/SingleThreadService.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/EmbeddedServiceI.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/EmbeddedServiceMethodBinderI.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/EmbeddedServiceMethodBinderT.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/EmbeddedThreadI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ReferenceContainer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/LinkedListHolder.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/Object.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/Reference.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ReferenceContainerFilter.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ReferenceContainerNode.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/TimeoutType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/StructuredDataI.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/EmbeddedThread.h \
../../../..//Common/UDP/UDPSProtocol.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/AdvancedErrorManagement.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ClassProperties.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/ErrorManagement.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/StreamMemoryReference.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/BufferedStreamI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/AnyType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/FormatDescriptor.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/IOBuffer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/CharBuffer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StreamI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Atomic.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/HighResolutionTimer.h \
TCPSConn.h TCPSFrameQueue.h TCPSServiceStream.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ReferenceT.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/StreamString.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/StreamStringIOBuffer.h \
../../../..//Source/Components/Interfaces/UDPStream/UDPSTransportI.h \
TCPSWorker.h
../../../..//Build/x86-linux/Components/Interfaces/TCPStreamService/TCPSWorker.o: TCPSWorker.cpp TCPSWorker.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/EmbeddedServiceMethodBinderI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/ErrorType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/CompilerTypes.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/Architecture/x86_gcc/CompilerTypes.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BitBoolean.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/TemplateParametersVerificator.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BitRange.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/TypeCharacteristics.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/ExecutionInfo.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BitRange.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BitBoolean.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L1Portability/Threads.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/ThreadsB.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/GeneralDefinitions.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/TypeCharacteristics.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/GeneralDefinitions.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/GeneralDefinitions.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L1Portability/ExceptionHandler.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/CompilerTypes.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/ProcessorType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BitSet.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/ExecutionInfo.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/FastPollingMutexSem.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Atomic.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Architecture/x86_gcc/AtomicA.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/ErrorManagement.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/ErrorInformation.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StreamI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/TimeoutType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/HighResolutionTimer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/TimeStamp.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Architecture/x86_gcc/HighResolutionTimerA.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/HighResolutionTimer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/../../HighResolutionTimer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/HighResolutionTimerCalibrator.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/../../GeneralDefinitions.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/TimeStamp.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Sleep.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/SingleThreadService.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/EmbeddedServiceI.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/EmbeddedServiceMethodBinderI.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/EmbeddedServiceMethodBinderT.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/EmbeddedThreadI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/Object.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/HeapI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/HeapManager.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/HeapI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ClassProperties.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/FractionalInteger.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ClassRegistryItem.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/CString.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/ZeroTerminatedArray.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/Introspection.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/IntrospectionEntry.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/TypeDescriptor.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BasicType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/FractionalInteger.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/ZeroTerminatedArray.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/LinkedListable.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/LinkedListHolderT.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/IteratorT.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/Iterator.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/LinkedListable.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/LinkedListHolder.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/SortFilter.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/SearchFilter.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/LoadableLibrary.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ObjectBuilder.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StringHelper.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/StructuredDataI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/AnyType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ClassRegistryDatabase.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/GlobalObjectsDatabase.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/GlobalObjectI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/StandardHeap.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/../../HeapI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/../Generic/StandardHeap_Generic.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StaticList.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StaticListHolder.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/Matrix.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/Vector.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/MemoryOperationsHelper.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/CLASSREGISTER.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ClassRegistryItemT.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ObjectBuilderT.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ReferenceContainer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/LinkedListHolder.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/Object.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/Reference.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ReferenceContainerFilter.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ReferenceContainerNode.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/TimeoutType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/StructuredDataI.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/EmbeddedThread.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/AdvancedErrorManagement.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ClassProperties.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/ErrorManagement.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/StreamMemoryReference.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/BufferedStreamI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/AnyType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/FormatDescriptor.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/IOBuffer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/CharBuffer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StreamI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Atomic.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/HighResolutionTimer.h \
TCPSConn.h TCPSFrameQueue.h TCPStreamService.h \
../../../..//Common/UDP/UDPSProtocol.h
@@ -1,353 +0,0 @@
TCPSFrameQueue.o: TCPSFrameQueue.cpp TCPSFrameQueue.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/CompilerTypes.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/Architecture/x86_gcc/CompilerTypes.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/HighResolutionTimer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/GeneralDefinitions.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/TypeCharacteristics.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/CompilerTypes.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/GeneralDefinitions.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/TimeStamp.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Architecture/x86_gcc/HighResolutionTimerA.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/HighResolutionTimer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/../../HighResolutionTimer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/HighResolutionTimerCalibrator.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/../../GeneralDefinitions.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/TimeStamp.h
TCPSServiceStream.o: TCPSServiceStream.cpp TCPSServiceStream.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/FastPollingMutexSem.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Atomic.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/GeneralDefinitions.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/TypeCharacteristics.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/CompilerTypes.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/Architecture/x86_gcc/CompilerTypes.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/GeneralDefinitions.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Architecture/x86_gcc/AtomicA.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/CompilerTypes.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/ErrorManagement.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/ErrorInformation.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/ErrorType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BitBoolean.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/TemplateParametersVerificator.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BitRange.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/TypeCharacteristics.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StreamI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/TimeoutType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/HighResolutionTimer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/TimeStamp.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Architecture/x86_gcc/HighResolutionTimerA.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/HighResolutionTimer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/../../HighResolutionTimer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/HighResolutionTimerCalibrator.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/../../GeneralDefinitions.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/TimeStamp.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Sleep.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ReferenceT.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/Reference.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/Object.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/HeapI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/HeapManager.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/HeapI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ClassProperties.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/GeneralDefinitions.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/FractionalInteger.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ClassRegistryItem.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/CString.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/ZeroTerminatedArray.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/Introspection.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/IntrospectionEntry.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/TypeDescriptor.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BasicType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/FractionalInteger.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BitRange.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BitBoolean.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/ZeroTerminatedArray.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/LinkedListable.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/LinkedListHolderT.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/IteratorT.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/Iterator.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/LinkedListable.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/LinkedListHolder.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/SortFilter.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/SearchFilter.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/LoadableLibrary.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ObjectBuilder.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StringHelper.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/StructuredDataI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/AnyType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ClassRegistryDatabase.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/GlobalObjectsDatabase.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/GlobalObjectI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/StandardHeap.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/../../HeapI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/../Generic/StandardHeap_Generic.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StaticList.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StaticListHolder.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/Matrix.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/Vector.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/MemoryOperationsHelper.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/CLASSREGISTER.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ClassRegistryItemT.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ObjectBuilderT.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/StreamString.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/CharBuffer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/ErrorManagement.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/BufferedStreamI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/TimeoutType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/AnyType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/FormatDescriptor.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/IOBuffer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StreamI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/StreamStringIOBuffer.h \
TCPStreamService.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/EmbeddedServiceMethodBinderI.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/ExecutionInfo.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L1Portability/Threads.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/ThreadsB.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L1Portability/ExceptionHandler.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/ProcessorType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BitSet.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/ExecutionInfo.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/Object.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/SingleThreadService.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/EmbeddedServiceI.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/EmbeddedServiceMethodBinderI.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/EmbeddedServiceMethodBinderT.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/EmbeddedThreadI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ReferenceContainer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/LinkedListHolder.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ReferenceContainerFilter.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ReferenceContainerNode.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/StructuredDataI.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/EmbeddedThread.h \
../../../..//Common/UDP/UDPSProtocol.h \
../../../..//Source/Components/Interfaces/UDPStream/UDPSTransportI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/AdvancedErrorManagement.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ClassProperties.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/StreamMemoryReference.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ObjectRegistryDatabase.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ReferenceContainer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ReferenceT.h \
TCPSConn.h TCPSFrameQueue.h TCPSWorker.h
TCPStreamService.o: TCPStreamService.cpp TCPStreamService.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/EmbeddedServiceMethodBinderI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/ErrorType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/CompilerTypes.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/Architecture/x86_gcc/CompilerTypes.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BitBoolean.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/TemplateParametersVerificator.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BitRange.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/TypeCharacteristics.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/ExecutionInfo.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BitRange.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BitBoolean.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L1Portability/Threads.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/ThreadsB.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/GeneralDefinitions.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/TypeCharacteristics.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/GeneralDefinitions.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/GeneralDefinitions.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L1Portability/ExceptionHandler.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/CompilerTypes.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/ProcessorType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BitSet.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/ExecutionInfo.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/FastPollingMutexSem.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Atomic.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Architecture/x86_gcc/AtomicA.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/ErrorManagement.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/ErrorInformation.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StreamI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/TimeoutType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/HighResolutionTimer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/TimeStamp.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Architecture/x86_gcc/HighResolutionTimerA.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/HighResolutionTimer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/../../HighResolutionTimer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/HighResolutionTimerCalibrator.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/../../GeneralDefinitions.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/TimeStamp.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Sleep.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/Object.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/HeapI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/HeapManager.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/HeapI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ClassProperties.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/FractionalInteger.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ClassRegistryItem.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/CString.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/ZeroTerminatedArray.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/Introspection.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/IntrospectionEntry.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/TypeDescriptor.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BasicType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/FractionalInteger.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/ZeroTerminatedArray.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/LinkedListable.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/LinkedListHolderT.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/IteratorT.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/Iterator.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/LinkedListable.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/LinkedListHolder.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/SortFilter.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/SearchFilter.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/LoadableLibrary.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ObjectBuilder.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StringHelper.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/StructuredDataI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/AnyType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ClassRegistryDatabase.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/GlobalObjectsDatabase.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/GlobalObjectI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/StandardHeap.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/../../HeapI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/../Generic/StandardHeap_Generic.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StaticList.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StaticListHolder.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/Matrix.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/Vector.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/MemoryOperationsHelper.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/CLASSREGISTER.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ClassRegistryItemT.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ObjectBuilderT.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/SingleThreadService.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/EmbeddedServiceI.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/EmbeddedServiceMethodBinderI.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/EmbeddedServiceMethodBinderT.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/EmbeddedThreadI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ReferenceContainer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/LinkedListHolder.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/Object.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/Reference.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ReferenceContainerFilter.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ReferenceContainerNode.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/TimeoutType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/StructuredDataI.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/EmbeddedThread.h \
../../../..//Common/UDP/UDPSProtocol.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/AdvancedErrorManagement.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ClassProperties.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/ErrorManagement.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/StreamMemoryReference.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/BufferedStreamI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/AnyType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/FormatDescriptor.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/IOBuffer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/CharBuffer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StreamI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Atomic.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/HighResolutionTimer.h \
TCPSConn.h TCPSFrameQueue.h TCPSServiceStream.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ReferenceT.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/StreamString.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/StreamStringIOBuffer.h \
../../../..//Source/Components/Interfaces/UDPStream/UDPSTransportI.h \
TCPSWorker.h
TCPSWorker.o: TCPSWorker.cpp TCPSWorker.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/EmbeddedServiceMethodBinderI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/ErrorType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/CompilerTypes.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/Architecture/x86_gcc/CompilerTypes.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BitBoolean.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/TemplateParametersVerificator.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BitRange.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/TypeCharacteristics.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/ExecutionInfo.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BitRange.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BitBoolean.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L1Portability/Threads.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/ThreadsB.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/GeneralDefinitions.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/TypeCharacteristics.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/GeneralDefinitions.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/GeneralDefinitions.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L1Portability/ExceptionHandler.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/CompilerTypes.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/ProcessorType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BitSet.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/ExecutionInfo.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/FastPollingMutexSem.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Atomic.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Architecture/x86_gcc/AtomicA.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/ErrorManagement.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/ErrorInformation.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StreamI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/TimeoutType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/HighResolutionTimer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/TimeStamp.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Architecture/x86_gcc/HighResolutionTimerA.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/HighResolutionTimer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/../../HighResolutionTimer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/HighResolutionTimerCalibrator.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/../../GeneralDefinitions.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/TimeStamp.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Sleep.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/SingleThreadService.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/EmbeddedServiceI.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/EmbeddedServiceMethodBinderI.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/EmbeddedServiceMethodBinderT.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/EmbeddedThreadI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/Object.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/HeapI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/HeapManager.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/HeapI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ClassProperties.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/FractionalInteger.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ClassRegistryItem.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/CString.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/ZeroTerminatedArray.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/Introspection.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/IntrospectionEntry.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/TypeDescriptor.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/BasicType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/FractionalInteger.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/ZeroTerminatedArray.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/LinkedListable.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/LinkedListHolderT.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/IteratorT.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/Iterator.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/LinkedListable.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/LinkedListHolder.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/SortFilter.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/SearchFilter.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/LoadableLibrary.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ObjectBuilder.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StringHelper.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/StructuredDataI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/AnyType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ClassRegistryDatabase.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/GlobalObjectsDatabase.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/GlobalObjectI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/StandardHeap.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/../../HeapI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Environment/Linux/../Generic/StandardHeap_Generic.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StaticList.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StaticListHolder.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/Matrix.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/Vector.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/MemoryOperationsHelper.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/CLASSREGISTER.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ClassRegistryItemT.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ObjectBuilderT.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ReferenceContainer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L0Types/LinkedListHolder.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/Object.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/Reference.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ReferenceContainerFilter.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ReferenceContainerNode.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/TimeoutType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/StructuredDataI.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/EmbeddedThread.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/AdvancedErrorManagement.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/ClassProperties.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/ErrorManagement.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/StreamMemoryReference.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/BufferedStreamI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L2Objects/AnyType.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/FormatDescriptor.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/IOBuffer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L3Streams/CharBuffer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/StreamI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Atomic.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/HighResolutionTimer.h \
TCPSConn.h TCPSFrameQueue.h TCPStreamService.h \
../../../..//Common/UDP/UDPSProtocol.h
@@ -1,7 +1,7 @@
############################################################# #############################################################
# MARTe2 Integrated Components — UDPStream # MARTe2 Integrated Components — UDPStream
############################################################# #############################################################
OBJSX=UDPSServer.x UDPSClient.x TCPSServer.x TCPSClient.x OBJSX=UDPSServer.x UDPSClient.x
PACKAGE=Components/Interfaces PACKAGE=Components/Interfaces
@@ -1,517 +0,0 @@
/**
* @file TCPSClient.cpp
* @brief Implementation of TCPSClient — UDPS framing over TCP, consumer side.
*/
#include "TCPSClient.h"
#include "AdvancedErrorManagement.h"
#include "ErrorType.h"
#include "HighResolutionTimer.h"
#include "Sleep.h"
#include <arpa/inet.h>
#include <errno.h>
#include <fcntl.h>
#include <netdb.h>
#include <netinet/in.h>
#include <poll.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
namespace MARTe {
const uint32 TCPSClient::TCPS_CLIENT_DEFAULT_RECONNECT_DELAY_S;
/** Longest single wait inside Execute(), so Stop() is prompt. */
static const int TCPS_CLIENT_SLICE_MS = 50;
/** Total time allowed for one connect() attempt. */
static const int TCPS_CLIENT_CONNECT_TIMEOUT_MS = 2000;
TCPSClient::TCPSClient()
: serverPort(0u), silenceTimeoutTicks(0u), reconnectDelayTicks(0u),
cpuMask(0xFFFFFFFFu), stackSize(65536u),
numStreams(1u), startRefs(0u), threadService(*this), fd(-1),
connected(false), stopping(false), lastDataTicks(0u), disconnectTick(0u),
connectionCount(0u), protocolErrors(0u), rxBuf(NULL_PTR(uint8 *)),
rxHave(0u), rxWant(UDPS_HEADER_SIZE), rxHeaderOk(false)
{
for (uint32 i = 0u; i < UDPS_TCP_MAX_STREAMS; i++) {
listeners[i] = NULL_PTR(UDPSClientListener *);
facades[i] = NULL_PTR(TCPSClientStream *);
lastDataCounter[i] = 0u;
lastDataCounterValid[i] = false;
lastDataGap[i] = 0u;
}
reconnectDelayTicks = static_cast<uint64>(TCPS_CLIENT_DEFAULT_RECONNECT_DELAY_S) *
HighResolutionTimer::Frequency();
}
TCPSClient::~TCPSClient() {
(void)Stop();
for (uint32 i = 0u; i < UDPS_TCP_MAX_STREAMS; i++) {
if (facades[i] != NULL_PTR(TCPSClientStream *)) {
delete facades[i];
facades[i] = NULL_PTR(TCPSClientStream *);
}
}
if (rxBuf != NULL_PTR(uint8 *)) {
delete[] rxBuf;
rxBuf = NULL_PTR(uint8 *);
}
}
bool TCPSClient::Initialise(StructuredDataI &data) {
bool ok = true;
if (!data.Read("ServerAddr", serverAddr) || (serverAddr.Size() == 0u)) {
serverAddr = "127.0.0.1";
}
if (!data.Read("Port", serverPort) || (serverPort == 0u) || (serverPort > 65535u)) {
REPORT_ERROR_STATIC(ErrorManagement::ParametersError,
"TCPSClient: a valid Port (1..65535) is required.");
ok = false;
}
streamName = "";
numStreams = 1u;
if (data.Read("StreamName", streamName)) {
ok = ParseStreamList();
}
float32 silence = 0.0f;
(void)data.Read("SilenceTimeout", silence);
if (silence < 0.0f) {
REPORT_ERROR_STATIC(ErrorManagement::ParametersError,
"TCPSClient: SilenceTimeout must be >= 0.");
ok = false;
silence = 0.0f;
}
silenceTimeoutTicks = static_cast<uint64>(
static_cast<float64>(silence) * static_cast<float64>(HighResolutionTimer::Frequency()));
float32 delay = static_cast<float32>(TCPS_CLIENT_DEFAULT_RECONNECT_DELAY_S);
(void)data.Read("ReconnectDelay", delay);
if (delay < 0.0f) {
REPORT_ERROR_STATIC(ErrorManagement::ParametersError,
"TCPSClient: ReconnectDelay must be >= 0.");
ok = false;
delay = 0.0f;
}
reconnectDelayTicks = static_cast<uint64>(
static_cast<float64>(delay) * static_cast<float64>(HighResolutionTimer::Frequency()));
(void)data.Read("CPUMask", cpuMask);
(void)data.Read("StackSize", stackSize);
return ok;
}
/** Validate streamName as "a" or "a,b,c" and set numStreams. */
bool TCPSClient::ParseStreamList() {
bool ok = (streamName.Size() <= UDPS_TCP_MAX_SUBSCRIBE_PAYLOAD);
const char8 *s = streamName.Buffer();
uint32 n = 0u;
while (ok && (s != NULL_PTR(const char8 *)) && (streamName.Size() > 0u)) {
const char8 *comma = strchr(s, ',');
const uint32 len = (comma != NULL_PTR(const char8 *)) ? static_cast<uint32>(comma - s)
: static_cast<uint32>(strlen(s));
ok = (len > 0u) && (len <= 255u) && (n < UDPS_TCP_MAX_STREAMS);
// Duplicates would be refused by the server; catch them here.
const char8 *t = streamName.Buffer();
for (uint32 k = 0u; ok && (k < n); k++) {
const char8 *tc = strchr(t, ',');
const uint32 tl = (tc != NULL_PTR(const char8 *)) ? static_cast<uint32>(tc - t)
: static_cast<uint32>(strlen(t));
if ((tl == len) && (strncmp(t, s, len) == 0)) {
ok = false;
}
t = (tc != NULL_PTR(const char8 *)) ? (tc + 1) : t;
}
n++;
s = (comma != NULL_PTR(const char8 *)) ? (comma + 1) : NULL_PTR(const char8 *);
}
if (!ok) {
REPORT_ERROR_STATIC(ErrorManagement::ParametersError,
"TCPSClient: StreamName must be 1..%u distinct names (1..255 chars each, "
"comma separated, %u bytes max).", UDPS_TCP_MAX_STREAMS,
UDPS_TCP_MAX_SUBSCRIBE_PAYLOAD);
} else if (n > 0u) {
numStreams = n;
}
return ok;
}
void TCPSClient::SetListener(UDPSClientListener *newListener) {
listeners[0] = newListener;
}
void TCPSClient::SetStreamListener(uint32 index, UDPSClientListener *newListener) {
if (index < UDPS_TCP_MAX_STREAMS) {
listeners[index] = newListener;
}
}
uint32 TCPSClient::GetNumStreams() const {
return numStreams;
}
UDPSClientI *TCPSClient::GetStream(uint32 index) {
if (index >= numStreams) {
return NULL_PTR(UDPSClientI *);
}
if (facades[index] == NULL_PTR(TCPSClientStream *)) {
facades[index] = new TCPSClientStream(*this, index);
}
return facades[index];
}
uint32 TCPSClient::GetStreamDataGap(uint32 index) const {
return (index < UDPS_TCP_MAX_STREAMS) ? lastDataGap[index] : 0u;
}
bool TCPSClient::AcquireStart() {
bool ok = true;
refMutex.FastLock();
if (startRefs == 0u) {
ok = Start();
}
if (ok) {
startRefs++;
}
refMutex.FastUnLock();
return ok;
}
bool TCPSClient::ReleaseStop() {
bool ok = true;
refMutex.FastLock();
if (startRefs > 0u) {
startRefs--;
if (startRefs == 0u) {
ok = Stop();
}
}
refMutex.FastUnLock();
return ok;
}
TCPSClientStream::TCPSClientStream(TCPSClient &o, uint32 i) : owner(o), index(i), started(false) {
}
TCPSClientStream::~TCPSClientStream() {
(void)Stop();
}
bool TCPSClientStream::Initialise(StructuredDataI &data) {
(void)data;
return true;
}
void TCPSClientStream::SetListener(UDPSClientListener *newListener) {
owner.SetStreamListener(index, newListener);
}
bool TCPSClientStream::Start() {
bool ok = true;
if (!started) {
ok = owner.AcquireStart();
started = ok;
}
return ok;
}
bool TCPSClientStream::Stop() {
bool ok = true;
if (started) {
started = false;
ok = owner.ReleaseStop();
}
return ok;
}
uint32 TCPSClientStream::GetLastDataGap() const {
return owner.GetStreamDataGap(index);
}
bool TCPSClient::Start() {
if (rxBuf == NULL_PTR(uint8 *)) {
rxBuf = new uint8[UDPS_HEADER_SIZE + UDPSClient::UDPS_CLIENT_MAX_PACKET_BYTES];
}
stopping = false;
connectionCount = 0u;
disconnectTick = 0u;
threadService.SetCPUMask(cpuMask);
threadService.SetStackSize(stackSize);
ErrorManagement::ErrorType err = threadService.Start();
return (err == ErrorManagement::NoError);
}
bool TCPSClient::Stop() {
stopping = true;
ErrorManagement::ErrorType err = threadService.Stop();
Disconnect();
return (err == ErrorManagement::NoError);
}
bool TCPSClient::IsConnected() const {
return connected;
}
uint32 TCPSClient::GetLastDataGap() const {
return lastDataGap[0];
}
uint32 TCPSClient::GetConnectionCount() const {
return connectionCount;
}
uint32 TCPSClient::GetProtocolErrors() const {
return protocolErrors;
}
// ---------------------------------------------------------------------------
// Thread entry
// ---------------------------------------------------------------------------
ErrorManagement::ErrorType TCPSClient::Execute(ExecutionInfo &info) {
if (info.GetStage() == ExecutionInfo::TerminationStage) {
Disconnect();
return ErrorManagement::NoError;
}
if (info.GetStage() != ExecutionInfo::MainStage) {
return ErrorManagement::NoError;
}
if (!connected) {
const uint64 now = HighResolutionTimer::Counter();
if ((disconnectTick == 0u) || ((now - disconnectTick) >= reconnectDelayTicks)) {
if (!Connect()) {
disconnectTick = HighResolutionTimer::Counter();
}
} else {
Sleep::MSec(10u);
}
return ErrorManagement::NoError;
}
if (!ReceiveStep()) {
Disconnect();
return ErrorManagement::NoError;
}
if (silenceTimeoutTicks > 0u) {
const uint64 now = HighResolutionTimer::Counter();
if ((now > lastDataTicks) && ((now - lastDataTicks) >= silenceTimeoutTicks)) {
REPORT_ERROR_STATIC(ErrorManagement::Warning,
"TCPSClient: server silent; reconnecting.");
Disconnect();
}
}
return ErrorManagement::NoError;
}
// ---------------------------------------------------------------------------
// Connection management
// ---------------------------------------------------------------------------
bool TCPSClient::Connect() {
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(static_cast<uint16>(serverPort));
if (inet_pton(AF_INET, serverAddr.Buffer(), &addr.sin_addr) != 1) {
struct addrinfo hints;
struct addrinfo *res = NULL_PTR(struct addrinfo *);
memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_STREAM;
if ((getaddrinfo(serverAddr.Buffer(), NULL_PTR(const char8 *), &hints, &res) != 0) ||
(res == NULL_PTR(struct addrinfo *))) {
REPORT_ERROR_STATIC(ErrorManagement::Warning,
"TCPSClient: cannot resolve '%s'.", serverAddr.Buffer());
return false;
}
addr.sin_addr = reinterpret_cast<struct sockaddr_in *>(res->ai_addr)->sin_addr;
freeaddrinfo(res);
}
int s = socket(AF_INET, SOCK_STREAM, 0);
if (s < 0) {
return false;
}
int flags = fcntl(s, F_GETFL, 0);
if ((flags < 0) || (fcntl(s, F_SETFL, flags | O_NONBLOCK) != 0)) {
(void)close(s);
return false;
}
bool ok = true;
int r = connect(s, reinterpret_cast<struct sockaddr *>(&addr), sizeof(addr));
if ((r != 0) && (errno == EINPROGRESS)) {
ok = false;
for (int waited = 0; (waited < TCPS_CLIENT_CONNECT_TIMEOUT_MS) && !stopping;
waited += TCPS_CLIENT_SLICE_MS) {
struct pollfd p;
p.fd = s;
p.events = POLLOUT;
p.revents = 0;
int pr = poll(&p, 1, TCPS_CLIENT_SLICE_MS);
if (pr > 0) {
int soErr = 0;
socklen_t len = static_cast<socklen_t>(sizeof(soErr));
ok = (getsockopt(s, SOL_SOCKET, SO_ERROR, &soErr, &len) == 0) && (soErr == 0);
break;
}
if ((pr < 0) && (errno != EINTR)) {
break;
}
}
} else if (r != 0) {
ok = false;
}
if (!ok) {
(void)close(s);
return false;
}
if (streamName.Size() > 0u) {
// A few dozen bytes on a fresh socket: never blocks.
uint8 sub[UDPS_HEADER_SIZE + UDPS_TCP_MAX_SUBSCRIBE_PAYLOAD];
const uint32 n = static_cast<uint32>(streamName.Size());
UDPSBuildHeader(sub, UDPS_TYPE_SUBSCRIBE, 0u, 0u, 1u, n);
memcpy(&sub[UDPS_HEADER_SIZE], streamName.Buffer(), n);
if (send(s, sub, UDPS_HEADER_SIZE + n, MSG_NOSIGNAL) != static_cast<ssize_t>(UDPS_HEADER_SIZE + n)) {
(void)close(s);
return false;
}
}
fd = s;
rxHave = 0u;
rxWant = UDPS_HEADER_SIZE;
rxHeaderOk = false;
for (uint32 i = 0u; i < UDPS_TCP_MAX_STREAMS; i++) {
lastDataCounterValid[i] = false;
}
lastDataTicks = HighResolutionTimer::Counter();
connectionCount++;
connected = true;
for (uint32 i = 0u; i < numStreams; i++) {
if (listeners[i] != NULL_PTR(UDPSClientListener *)) {
listeners[i]->OnUDPSConnected();
}
}
return true;
}
void TCPSClient::Disconnect() {
const bool was = connected;
connected = false;
if (fd >= 0) {
(void)close(fd);
fd = -1;
}
rxHave = 0u;
rxWant = UDPS_HEADER_SIZE;
rxHeaderOk = false;
disconnectTick = HighResolutionTimer::Counter();
for (uint32 i = 0u; was && (i < numStreams); i++) {
if (listeners[i] != NULL_PTR(UDPSClientListener *)) {
listeners[i]->OnUDPSDisconnected();
}
}
}
// ---------------------------------------------------------------------------
// Frame reader
// ---------------------------------------------------------------------------
bool TCPSClient::ReceiveStep() {
struct pollfd p;
p.fd = fd;
p.events = POLLIN;
p.revents = 0;
int pr = poll(&p, 1, TCPS_CLIENT_SLICE_MS);
if (pr < 0) {
return (errno == EINTR);
}
if (pr == 0) {
return true;
}
ssize_t n = recv(fd, &rxBuf[rxHave], rxWant - rxHave, MSG_DONTWAIT);
if (n == 0) {
REPORT_ERROR_STATIC(ErrorManagement::Information,
"TCPSClient: server closed the connection.");
return false;
}
if (n < 0) {
if ((errno == EAGAIN) || (errno == EWOULDBLOCK) || (errno == EINTR)) {
return true;
}
REPORT_ERROR_STATIC(ErrorManagement::Warning,
"TCPSClient: recv failed: %s.", strerror(errno));
return false;
}
rxHave += static_cast<uint32>(n);
lastDataTicks = HighResolutionTimer::Counter();
if (!rxHeaderOk && (rxHave >= UDPS_HEADER_SIZE)) {
UDPSPacketHeader h;
memcpy(&h, rxBuf, UDPS_HEADER_SIZE);
if ((h.magic != UDPS_MAGIC) ||
(h.payloadBytes > UDPSClient::UDPS_CLIENT_MAX_PACKET_BYTES) ||
(h.totalFragments != 1u) || (h.fragmentIdx >= numStreams)) {
REPORT_ERROR_STATIC(ErrorManagement::Warning,
"TCPSClient: invalid frame header (magic 0x%x, %u payload bytes); reconnecting.",
h.magic, h.payloadBytes);
protocolErrors++;
return false;
}
rxHeaderOk = true;
rxWant = UDPS_HEADER_SIZE + h.payloadBytes;
}
if (rxHeaderOk && (rxHave == rxWant)) {
const bool ok = Dispatch();
rxHave = 0u;
rxWant = UDPS_HEADER_SIZE;
rxHeaderOk = false;
return ok;
}
return true;
}
bool TCPSClient::Dispatch() {
UDPSPacketHeader h;
memcpy(&h, rxBuf, UDPS_HEADER_SIZE);
const uint8 *payload = &rxBuf[UDPS_HEADER_SIZE];
const uint32 idx = h.fragmentIdx; // validated < numStreams
UDPSClientListener *listener = listeners[idx];
if (listener != NULL_PTR(UDPSClientListener *)) {
if (h.type == UDPS_TYPE_CONFIG) {
listener->OnUDPSConfig(payload, h.payloadBytes);
} else if (h.type == UDPS_TYPE_DATA) {
lastDataGap[idx] = 0u;
if (lastDataCounterValid[idx]) {
const int32 delta = static_cast<int32>(h.counter - lastDataCounter[idx]);
if (delta > 1) {
lastDataGap[idx] = static_cast<uint32>(delta) - 1u;
}
}
lastDataCounter[idx] = h.counter;
lastDataCounterValid[idx] = true;
listener->OnUDPSFragment(h.counter, rxWant, true);
listener->OnUDPSData(payload, h.payloadBytes);
} else {
// Unknown / client-to-server types are ignored (forward compatible).
}
}
return true;
}
} // namespace MARTe
@@ -1,163 +0,0 @@
#ifndef TCPS_CLIENT_H_
#define TCPS_CLIENT_H_
/**
* @file TCPSClient.h
* @brief Auto-reconnecting TCP receiver for the UDPS wire format.
*
* TCPSClient is the consumer-side counterpart of TCPSServer. It connects to a
* TCPSServer, reads UDPS frames (17-byte header + payload) from the byte stream
* and delivers CONFIG / DATA payloads through UDPSClientListener, so any
* listener written for UDPSClient works unchanged.
*
* It runs its own thread (SingleThreadService); every wait inside Execute() is
* bounded by 50 ms so Stop() is prompt. A partial frame survives across
* iterations. Bad magic, an oversize length, or a fragmented frame means the
* stream cannot be trusted any more: the connection is dropped and re-opened.
*
* There is no CONNECT / ACK / keepalive traffic: the TCP connection is the
* session and the server sends CONFIG on accept.
*/
#include "EmbeddedServiceMethodBinderI.h"
#include "FastPollingMutexSem.h"
#include "ExecutionInfo.h"
#include "SingleThreadService.h"
#include "StreamString.h"
#include "StructuredDataI.h"
#include "UDPSClient.h"
#include "UDPSProtocol.h"
#include "UDPSTransportI.h"
namespace MARTe {
class TCPSClient;
/**
* One stream of a (possibly multiplexed) TCPSClient connection, seen as an
* independent UDPSClientI. Obtained from TCPSClient::GetStream(). Start()/Stop()
* are reference counted so the shared connection runs while any stream wants it.
*/
class TCPSClientStream : public UDPSClientI {
public:
TCPSClientStream(TCPSClient &owner, uint32 index);
virtual ~TCPSClientStream();
/** The connection is configured through its TCPSClient; always succeeds. */
virtual bool Initialise(StructuredDataI &data);
virtual void SetListener(UDPSClientListener *newListener);
virtual bool Start();
virtual bool Stop();
virtual uint32 GetLastDataGap() const;
private:
TCPSClient &owner;
uint32 index;
bool started;
};
class TCPSClient : public UDPSClientI, public EmbeddedServiceMethodBinderI {
public:
/** Most streams one connection can multiplex. */
static const uint32 TCPS_CLIENT_MAX_STREAMS = UDPS_TCP_MAX_STREAMS;
/** Default delay between reconnect attempts (seconds). */
static const uint32 TCPS_CLIENT_DEFAULT_RECONNECT_DELAY_S = 2u;
TCPSClient();
virtual ~TCPSClient();
/**
* Keys:
* - ServerAddr (char*) IPv4 address or host name. Default 127.0.0.1.
* - Port (uint32) Server TCP port. Required.
* - StreamName (char*) Optional. When set, a SUBSCRIBE frame carrying
* this name is sent right after every connect, to
* select one stream of a TCPStreamService. A plain
* TCPSServer ignores (discards) it. A comma
* separated list ("a,b,c") multiplexes several
* streams over this one connection; stream i of
* the list is reached through GetStream(i).
* - SilenceTimeout (float32) Seconds without any received byte before the
* connection is considered dead. Default 0
* (disabled: TCP reports a dead peer itself).
* - ReconnectDelay (float32) Seconds between connection attempts. Default 2.
* - CPUMask (uint32) Receive thread affinity. Default 0xFFFFFFFF.
* - StackSize (uint32) Receive thread stack. Default 65536.
*/
virtual bool Initialise(StructuredDataI &data);
virtual void SetListener(UDPSClientListener *newListener);
/** Listener of stream @p index (0 = SetListener()). Call before Start(). */
void SetStreamListener(uint32 index, UDPSClientListener *newListener);
/** @return number of streams subscribed to (1 unless StreamName is a list). */
uint32 GetNumStreams() const;
/**
* @return a UDPSClientI for stream @p index, owned by this client (NULL if out of
* range). Valid after Initialise().
*/
UDPSClientI *GetStream(uint32 index);
/** Counter gap of the DATA frame of stream @p index being delivered. */
uint32 GetStreamDataGap(uint32 index) const;
/** Reference-counted Start()/Stop() used by the TCPSClientStream facades. */
bool AcquireStart();
bool ReleaseStop();
virtual bool Start();
virtual bool Stop();
/** Thread entry point; do not call directly. */
virtual ErrorManagement::ErrorType Execute(ExecutionInfo &info);
/** Packets the server dropped for this client just before the DATA frame being delivered. */
virtual uint32 GetLastDataGap() const;
/** @return true while a connection is established. */
bool IsConnected() const;
/** @return connections established since Start(). */
uint32 GetConnectionCount() const;
/** @return connections dropped because of a protocol violation. */
uint32 GetProtocolErrors() const;
private:
bool Connect();
void Disconnect();
/** One bounded receive step. @return false when the connection is lost. */
bool ReceiveStep();
/** Deliver the complete frame sitting in rxBuf. @return false on a protocol error. */
bool Dispatch();
/** Validate StreamName as one name or a comma separated list; sets numStreams. */
bool ParseStreamList();
StreamString serverAddr;
StreamString streamName;
uint32 serverPort;
uint64 silenceTimeoutTicks;
uint64 reconnectDelayTicks;
uint32 cpuMask;
uint32 stackSize;
uint32 numStreams;
UDPSClientListener *listeners[UDPS_TCP_MAX_STREAMS];
TCPSClientStream *facades[UDPS_TCP_MAX_STREAMS];
FastPollingMutexSem refMutex;
uint32 startRefs;
SingleThreadService threadService;
int fd;
volatile bool connected;
volatile bool stopping;
uint64 lastDataTicks;
uint64 disconnectTick;
volatile uint32 connectionCount;
volatile uint32 protocolErrors;
uint8 *rxBuf; ///< header + payload of the frame being received
uint32 rxHave; ///< bytes of the current frame received so far
uint32 rxWant; ///< total bytes the current frame needs (header first)
bool rxHeaderOk; ///< header validated; rxWant covers the payload
uint32 lastDataCounter[UDPS_TCP_MAX_STREAMS]; ///< counter of the last delivered DATA frame
bool lastDataCounterValid[UDPS_TCP_MAX_STREAMS]; ///< false until the first DATA frame of a connection
uint32 lastDataGap[UDPS_TCP_MAX_STREAMS]; ///< frames missing before the one being delivered
};
} // namespace MARTe
#endif // TCPS_CLIENT_H_
Loaded 100 of 141 files, more files were not shown because too many files have changed in this diff. Show more