Compare commits

...

2 Commits

Author SHA1 Message Date
Martino Ferrari 0412c20edd Implemented client datasource 2026-06-25 00:45:45 +02:00
Martino Ferrari dca4872976 Add design spec for StreamHub binary recorder
Lossless packet-decode capture to FileWriter-compatible per-source binary
files with size-capped rotation, config + WS control, and double-buffered
flush on the push thread (Approach C).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-25 00:45:25 +02:00
29 changed files with 3636 additions and 618 deletions
+318 -111
View File
@@ -75,9 +75,21 @@ void App::update() {
if (nowConnected && !prevConnected_) { if (nowConnected && !prevConnected_) {
ws_.sendText(BuildGetSources()); ws_.sendText(BuildGetSources());
ws_.sendText(BuildGetStats()); ws_.sendText(BuildGetStats());
ws_.sendText(BuildHistoryInfo());
} }
prevConnected_ = nowConnected; prevConnected_ = nowConnected;
/* Re-request history info periodically until signals are populated
* (the hub opens history files after the first data packet, which is
* after our initial connect-time query). */
if (nowConnected && historyInfo_.enabled && historyInfo_.signals.empty()) {
double now = ImGui::GetTime();
if (now - lastHistInfoReq_ > 2.0) {
ws_.sendText(BuildHistoryInfo());
lastHistInfoReq_ = now;
}
}
/* Drain WebSocket receive queue */ /* Drain WebSocket receive queue */
ws_.poll([this](const WSMessage& msg) { ws_.poll([this](const WSMessage& msg) {
if (msg.isBinary) { if (msg.isBinary) {
@@ -99,8 +111,7 @@ void App::update() {
| ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoResize
| ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoMove
| ImGuiWindowFlags_NoBringToFrontOnFocus | ImGuiWindowFlags_NoBringToFrontOnFocus
| ImGuiWindowFlags_NoNavFocus | ImGuiWindowFlags_NoNavFocus;
| ImGuiWindowFlags_MenuBar;
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f); ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f); ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
@@ -108,8 +119,8 @@ void App::update() {
ImGui::Begin("##main", nullptr, wf); ImGui::Begin("##main", nullptr, wf);
ImGui::PopStyleVar(3); ImGui::PopStyleVar(3);
renderMenuBar();
renderToolbar(); renderToolbar();
if (showHistBar_) { renderHistoryBar(); }
if (showTrigBar_) { renderTriggerBar(); } if (showTrigBar_) { renderTriggerBar(); }
/* Horizontal split: sidebar | plot grid */ /* Horizontal split: sidebar | plot grid */
@@ -136,90 +147,33 @@ void App::update() {
if (showAddSrc_) { renderAddSourceModal(); } if (showAddSrc_) { renderAddSourceModal(); }
} }
/* ── Menu bar ────────────────────────────────────────────────────────────── */ /* renderMenuBar() removed — all functionality merged into toolbar + sidebar */
void App::renderMenuBar() {
if (!ImGui::BeginMenuBar()) { return; }
if (ImGui::BeginMenu("File")) {
if (ImGui::MenuItem("Quit", "Alt+F4")) {
/* Signal quit via SDL event — handled in main.cpp via flag */
}
ImGui::EndMenu();
}
if (ImGui::BeginMenu("View")) {
ImGui::MenuItem("Sidebar", nullptr, &sidebarOpen_);
ImGui::MenuItem("Trigger Bar", nullptr, &showTrigBar_);
ImGui::MenuItem("Statistics", nullptr, &showStats_);
ImGui::EndMenu();
}
if (ImGui::BeginMenu("Sources")) {
if (ImGui::MenuItem(ICON_FA_PLUS " Add Source...")) { showAddSrc_ = true; }
ImGui::EndMenu();
}
if (ImGui::BeginMenu("Layout")) {
for (int i = 0; i < static_cast<int>(PlotLayout::kCount); i++) {
bool sel = (static_cast<int>(layout_) == i);
if (ImGui::MenuItem(PlotLayoutNames[i], nullptr, sel)) {
setLayout(static_cast<PlotLayout>(i));
}
}
ImGui::EndMenu();
}
/* Right-aligned: [host] : [port] [Connect/Reconnect] ● status */
bool connected = ws_.isConnected();
const char* btnLbl = connected ? "Reconnect" : "Connect";
const char* ledLbl = connected ? ICON_FA_CIRCLE " Connected"
: ICON_FA_CIRCLE " Disconnected";
/* Measure widget widths to right-align the whole group */
const ImGuiStyle& st = ImGui::GetStyle();
const float kHostW = 130.f;
const float kPortW = 52.f;
const float kBtnW = ImGui::CalcTextSize(btnLbl).x + st.FramePadding.x * 2.f;
const float kLedW = ImGui::CalcTextSize(ledLbl).x;
const float kGroupW = kHostW + 8.f + kPortW + 8.f + kBtnW + 8.f + kLedW;
ImGui::SetCursorPosX(
ImGui::GetCursorPosX() +
ImGui::GetContentRegionAvail().x - kGroupW);
ImGui::SetNextItemWidth(kHostW);
ImGui::InputText("##hubhost", hostBuf_, sizeof(hostBuf_));
ImGui::SameLine(0.f, 2.f);
ImGui::TextUnformatted(":");
ImGui::SameLine(0.f, 2.f);
ImGui::SetNextItemWidth(kPortW);
ImGui::InputText("##hubport", portBuf_, sizeof(portBuf_),
ImGuiInputTextFlags_CharsDecimal);
ImGui::SameLine(0.f, 6.f);
if (ImGui::Button(btnLbl)) {
ws_.reconnect(hostBuf_, static_cast<uint16_t>(atoi(portBuf_)));
}
ImGui::SameLine(0.f, 8.f);
ImVec4 ledColor = connected
? ImVec4(0.1f, 0.9f, 0.3f, 1.f)
: ImVec4(0.9f, 0.2f, 0.2f, 1.f);
ImGui::TextColored(ledColor, "%s", ledLbl);
ImGui::EndMenuBar();
}
/* ── Toolbar ─────────────────────────────────────────────────────────────── */ /* ── Toolbar ─────────────────────────────────────────────────────────────── */
void App::renderToolbar() { void App::renderToolbar() {
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(4.f, 4.f)); ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(4.f, 4.f));
/* Layout picker — pictogram popup */ /* ── Sidebar toggle (leftmost) ───────────────────────────────────────── */
{
if (sidebarOpen_) {
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.537f,0.706f,0.980f,0.4f));
}
if (ImGui::Button(ICON_FA_BARS "##sidebar")) { sidebarOpen_ = !sidebarOpen_; }
if (sidebarOpen_) { ImGui::PopStyleColor(); }
if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Toggle sidebar"); }
}
ImGui::SameLine();
/* ── Layout picker — pictogram popup ─────────────────────────────────── */
static const int kIconCols[] = {1,2,1,3,1,2,4,1}; static const int kIconCols[] = {1,2,1,3,1,2,4,1};
static const int kIconRows[] = {1,1,2,1,3,2,1,4}; static const int kIconRows[] = {1,1,2,1,3,2,1,4};
static const ImVec2 kIconSz = ImVec2(36.f, 24.f); static const ImVec2 kIconSz = ImVec2(36.f, 24.f);
if (ImGui::Button(ICON_FA_TABLE_CELLS_LARGE " Layout")) { if (ImGui::Button(ICON_FA_TABLE_CELLS_LARGE "##layout")) {
ImGui::OpenPopup("##layout_pop"); ImGui::OpenPopup("##layout_pop");
} }
if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Layout"); }
ImGui::SameLine(); ImGui::SameLine();
if (ImGui::BeginPopup("##layout_pop")) { if (ImGui::BeginPopup("##layout_pop")) {
ImGui::TextDisabled("Select layout"); ImGui::TextDisabled("Select layout");
@@ -228,7 +182,6 @@ void App::renderToolbar() {
for (int li = 0; li < static_cast<int>(PlotLayout::kCount); li++) { for (int li = 0; li < static_cast<int>(PlotLayout::kCount); li++) {
bool sel = (static_cast<int>(layout_) == li); bool sel = (static_cast<int>(layout_) == li);
ImVec2 p = ImGui::GetCursorScreenPos(); ImVec2 p = ImGui::GetCursorScreenPos();
/* Invisible button sized to the icon */
if (ImGui::InvisibleButton(PlotLayoutNames[li], kIconSz)) { if (ImGui::InvisibleButton(PlotLayoutNames[li], kIconSz)) {
setLayout(static_cast<PlotLayout>(li)); setLayout(static_cast<PlotLayout>(li));
ImGui::CloseCurrentPopup(); ImGui::CloseCurrentPopup();
@@ -242,7 +195,7 @@ void App::renderToolbar() {
ImGui::EndPopup(); ImGui::EndPopup();
} }
/* Global pause */ /* ── Global pause ────────────────────────────────────────────────────── */
if (ImGui::Button(globalPaused_ ? ICON_FA_PLAY " Resume" if (ImGui::Button(globalPaused_ ? ICON_FA_PLAY " Resume"
: ICON_FA_PAUSE " Pause")) { : ICON_FA_PAUSE " Pause")) {
globalPaused_ = !globalPaused_; globalPaused_ = !globalPaused_;
@@ -253,45 +206,77 @@ void App::renderToolbar() {
} }
ImGui::SameLine(); ImGui::SameLine();
/* Cursors A/B (global: shared & synchronised across all plots) */ /* ── Cursors A/B toggle ──────────────────────────────────────────────── */
{ {
if (cursorsOn_) { if (cursorsOn_) {
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.537f,0.706f,0.980f,0.4f)); ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.537f,0.706f,0.980f,0.4f));
} }
if (ImGui::Button(ICON_FA_CROSSHAIRS " Cursors")) { if (ImGui::Button(ICON_FA_CROSSHAIRS "##curs")) { cursorsOn_ = !cursorsOn_; }
cursorsOn_ = !cursorsOn_;
if (cursorsOn_) {
/* seed cursors at 25% / 75% of the live window */
const double wallNow = std::chrono::duration<double>(
std::chrono::system_clock::now().time_since_epoch()).count();
const double x0 = wallNow - windowSec_;
cursorA_ = x0 + windowSec_ * 0.25;
cursorB_ = x0 + windowSec_ * 0.75;
}
}
if (cursorsOn_) { ImGui::PopStyleColor(); } if (cursorsOn_) { ImGui::PopStyleColor(); }
if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Cursors A/B"); }
} }
ImGui::SameLine(); ImGui::SameLine();
/* Trigger bar toggle */ /* ── Trigger bar toggle ──────────────────────────────────────────────── */
if (ImGui::Button(ICON_FA_BOLT " Trigger")) { showTrigBar_ = !showTrigBar_; } {
if (showTrigBar_) {
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.537f,0.706f,0.980f,0.4f));
}
if (ImGui::Button(ICON_FA_BOLT "##trig")) { showTrigBar_ = !showTrigBar_; }
if (showTrigBar_) { ImGui::PopStyleColor(); }
if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Trigger"); }
}
ImGui::SameLine(); ImGui::SameLine();
/* Stats */ /* ── History browse ──────────────────────────────────────────────────── */
if (ImGui::Button(ICON_FA_CHART_COLUMN " Stats")) { showStats_ = !showStats_; } {
bool histAvail = historyInfo_.enabled && !historyInfo_.signals.empty();
if (!histAvail) { ImGui::BeginDisabled(); }
if (showHistBar_) {
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.537f,0.706f,0.980f,0.4f));
}
if (ImGui::Button(ICON_FA_CLOCK_ROTATE_LEFT "##hist")) {
if (histAvail) {
showHistBar_ = !showHistBar_;
if (showHistBar_) {
/* On first open, jump to the full history range */
double ht0 = 1e300, ht1 = -1e300;
for (const auto& hs : historyInfo_.signals) {
if (hs.t0 < ht0) ht0 = hs.t0;
if (hs.t1 > ht1) ht1 = hs.t1;
}
if (ht1 > ht0) {
for (int i = 0; i < numPlotSlots(); i++) {
liveFollow_[i] = false;
setPlotX(i, ht0, ht1);
zoomCache_[i].valid = false;
zoomCache_[i].pending = false;
histZoomCache_[i].valid = false;
histZoomCache_[i].pending = false;
zoomHist_[i].clear();
}
}
} else {
/* Closing the bar → back to live */
for (int i = 0; i < numPlotSlots(); i++) {
liveFollow_[i] = true;
}
}
}
}
if (showHistBar_) { ImGui::PopStyleColor(); }
if (!histAvail) { ImGui::EndDisabled(); }
if (ImGui::IsItemHovered()) { ImGui::SetTooltip("History browse"); }
}
ImGui::SameLine(); ImGui::SameLine();
/* Add source */ /* ── Time window presets ─────────────────────────────────────────────── */
if (ImGui::Button(ICON_FA_PLUS " Source")) { showAddSrc_ = true; }
ImGui::SameLine();
/* Time window presets (Ctrl+scroll on a plot still adjusts freely) */
static const double kWinPresets[] = {1.0, 5.0, 10.0, 30.0, 60.0}; static const double kWinPresets[] = {1.0, 5.0, 10.0, 30.0, 60.0};
static const char* kWinLabels[] = {"1 s", "5 s", "10 s", "30 s", "60 s"}; static const char* kWinLabels[] = {"1 s", "5 s", "10 s", "30 s", "60 s"};
char winPreview[16]; char winPreview[16];
snprintf(winPreview, sizeof(winPreview), "%.3g s", windowSec_); snprintf(winPreview, sizeof(winPreview), "%.3g s", windowSec_);
ImGui::SetNextItemWidth(70.f); ImGui::SetNextItemWidth(70.f);
if (ImGui::BeginCombo("Win", winPreview)) { if (ImGui::BeginCombo("##win", winPreview)) {
for (int wi = 0; wi < 5; wi++) { for (int wi = 0; wi < 5; wi++) {
bool sel = std::fabs(windowSec_ - kWinPresets[wi]) < 1e-9; bool sel = std::fabs(windowSec_ - kWinPresets[wi]) < 1e-9;
if (ImGui::Selectable(kWinLabels[wi], sel)) { if (ImGui::Selectable(kWinLabels[wi], sel)) {
@@ -301,18 +286,213 @@ void App::renderToolbar() {
} }
ImGui::EndCombo(); ImGui::EndCombo();
} }
/* ── Right-aligned group: [Stats] [Connection ▼] ● LED ──────────────── */
bool connected = ws_.isConnected();
const char* connLabel = connected
? ICON_FA_CIRCLE " Connected"
: ICON_FA_CIRCLE " Disconnected";
const ImGuiStyle& st = ImGui::GetStyle();
const float kStatsW = ImGui::CalcTextSize(ICON_FA_CHART_COLUMN).x
+ st.FramePadding.x * 2.f;
const float kConnBtnW = ImGui::CalcTextSize(ICON_FA_LINK).x
+ st.FramePadding.x * 2.f;
const float kLedW = ImGui::CalcTextSize(connLabel).x + 8.f;
const float kGroupW = kStatsW + 4.f + kConnBtnW + 4.f + kLedW;
float rightX = ImGui::GetCursorPosX() +
ImGui::GetContentRegionAvail().x - kGroupW;
if (rightX > ImGui::GetCursorPosX()) {
ImGui::SameLine(rightX);
} else {
ImGui::SameLine();
}
/* Stats toggle */
{
if (showStats_) {
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.537f,0.706f,0.980f,0.4f));
}
if (ImGui::Button(ICON_FA_CHART_COLUMN "##stats")) { showStats_ = !showStats_; }
if (showStats_) { ImGui::PopStyleColor(); }
if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Statistics"); }
}
ImGui::SameLine(0.f, 4.f);
/* Connection dropdown */
if (ImGui::Button(ICON_FA_LINK "##conn")) {
ImGui::OpenPopup("##conn_pop");
}
if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Connection"); }
if (ImGui::BeginPopup("##conn_pop")) {
ImGui::TextDisabled("Hub Connection");
ImGui::Separator();
ImGui::SetNextItemWidth(160.f);
ImGui::InputText("Host##hubhost", hostBuf_, sizeof(hostBuf_));
ImGui::SetNextItemWidth(80.f);
ImGui::InputText("Port##hubport", portBuf_, sizeof(portBuf_),
ImGuiInputTextFlags_CharsDecimal);
ImGui::Spacing();
const char* btnLbl = connected ? "Reconnect" : "Connect";
if (ImGui::Button(btnLbl, ImVec2(120.f, 0.f))) {
ws_.reconnect(hostBuf_, static_cast<uint16_t>(atoi(portBuf_)));
ImGui::CloseCurrentPopup();
}
ImGui::EndPopup();
}
ImGui::SameLine(0.f, 4.f);
/* Status LED */
ImVec4 ledColor = connected
? ImVec4(0.1f, 0.9f, 0.3f, 1.f)
: ImVec4(0.9f, 0.2f, 0.2f, 1.f);
ImGui::TextColored(ledColor, "%s", connLabel);
ImGui::PopStyleVar();
ImGui::Separator();
}
/* ── History browsing toolbar ────────────────────────────────────────────── */
void App::renderHistoryBar() {
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(4.f, 4.f));
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.980f,0.702f,0.529f,1.f));
/* Title */
ImGui::TextUnformatted(ICON_FA_CLOCK_ROTATE_LEFT " History");
ImGui::PopStyleColor();
ImGui::SameLine(); ImGui::SameLine();
/* Max points control */ /* ── Live button ── */
ImGui::SetNextItemWidth(80.f); if (ImGui::SmallButton(ICON_FA_TOWER_BROADCAST " Live")) {
int mp = static_cast<int>(maxPoints_); showHistBar_ = false;
if (ImGui::InputInt("MaxPts", &mp, 0, 0, for (int i = 0; i < numPlotSlots(); i++) {
ImGuiInputTextFlags_EnterReturnsTrue)) { liveFollow_[i] = true;
if (mp >= 2) { }
maxPoints_ = static_cast<uint32_t>(mp); ImGui::PopStyleVar();
ws_.sendText(BuildSetMaxPoints(maxPoints_)); return;
}
ImGui::SameLine();
ImGui::TextDisabled("|"); ImGui::SameLine();
/* ── Current time range display ── */
double t0 = 0.0, t1 = 0.0;
bool haveRange = false;
for (int i = 0; i < numPlotSlots(); i++) {
if (!liveFollow_[i]) {
t0 = plotXMin_[i]; t1 = plotXMax_[i];
haveRange = true;
break;
} }
} }
const double wallNow = std::chrono::duration<double>(
std::chrono::system_clock::now().time_since_epoch()).count();
if (haveRange) {
double span = t1 - t0;
double ago = wallNow - t1;
char rangeLabel[128];
if (ago < 1.0) {
snprintf(rangeLabel, sizeof(rangeLabel),
"%.3g s span | now", span);
} else if (ago < 60.0) {
snprintf(rangeLabel, sizeof(rangeLabel),
"%.3g s span | %.0f s ago", span, ago);
} else if (ago < 3600.0) {
snprintf(rangeLabel, sizeof(rangeLabel),
"%.3g s span | %.1f min ago", span, ago / 60.0);
} else {
snprintf(rangeLabel, sizeof(rangeLabel),
"%.3g s span | %.1f h ago", span, ago / 3600.0);
}
ImGui::TextDisabled("%s", rangeLabel);
} else {
ImGui::TextDisabled("No plot range");
}
ImGui::SameLine();
/* ── Navigation: pan ← → ── */
if (ImGui::SmallButton(ICON_FA_CHEVRON_LEFT "##hleft")) {
for (int i = 0; i < numPlotSlots(); i++) {
if (!liveFollow_[i]) {
double span = plotXMax_[i] - plotXMin_[i];
setPlotX(i, plotXMin_[i] - span * 0.75, plotXMax_[i] - span * 0.75);
}
}
}
if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Pan left (75%% of window)"); }
ImGui::SameLine(0.f, 2.f);
if (ImGui::SmallButton(ICON_FA_CHEVRON_RIGHT "##hright")) {
for (int i = 0; i < numPlotSlots(); i++) {
if (!liveFollow_[i]) {
double span = plotXMax_[i] - plotXMin_[i];
double newMax = plotXMax_[i] + span * 0.75;
/* Clamp: don't go past wallNow */
if (newMax > wallNow) {
newMax = wallNow;
double newMin = newMax - span;
setPlotX(i, newMin, newMax);
} else {
setPlotX(i, plotXMin_[i] + span * 0.75, newMax);
}
}
}
}
if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Pan right (75%% of window)"); }
ImGui::SameLine();
ImGui::TextDisabled("|"); ImGui::SameLine();
/* ── Jump presets ── */
static const double kJumpSec[] = { 10.0, 30.0, 60.0,
300.0, 600.0, 1800.0, 3600.0};
static const char* kJumpLabel[] = {"10 s", "30 s", "1 min",
"5 min","10 min","30 min","1 h"};
for (int j = 0; j < 7; j++) {
char jid[32];
snprintf(jid, sizeof(jid), "%s##hj%d", kJumpLabel[j], j);
if (ImGui::SmallButton(jid)) {
for (int i = 0; i < numPlotSlots(); i++) {
liveFollow_[i] = false;
double span = plotXMax_[i] - plotXMin_[i];
if (span <= 0.0) { span = windowSec_; }
setPlotX(i, wallNow - kJumpSec[j] - span,
wallNow - kJumpSec[j]);
}
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Jump to %s ago", kJumpLabel[j]);
}
if (j < 6) { ImGui::SameLine(0.f, 2.f); }
}
ImGui::SameLine();
/* ── All history ── */
bool canAll = historyInfo_.enabled && !historyInfo_.signals.empty();
if (!canAll) { ImGui::BeginDisabled(); }
if (ImGui::SmallButton("All")) {
double ht0 = 1e300, ht1 = -1e300;
for (const auto& hs : historyInfo_.signals) {
if (hs.t0 < ht0) ht0 = hs.t0;
if (hs.t1 > ht1) ht1 = hs.t1;
}
if (ht1 > ht0) {
for (int i = 0; i < numPlotSlots(); i++) {
liveFollow_[i] = false;
setPlotX(i, ht0, ht1);
zoomCache_[i].valid = false;
zoomCache_[i].pending = false;
histZoomCache_[i].valid = false;
histZoomCache_[i].pending = false;
zoomHist_[i].clear();
}
}
}
if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Show all available history"); }
if (!canAll) { ImGui::EndDisabled(); }
ImGui::PopStyleVar(); ImGui::PopStyleVar();
ImGui::Separator(); ImGui::Separator();
@@ -521,6 +701,7 @@ void App::handleBinary(const uint8_t* data, size_t len) {
trigger_.status = "triggered"; trigger_.status = "triggered";
trigger_.trigTime = capture_.trigTime; trigger_.trigTime = capture_.trigTime;
trigger_.hasTrigTime = true; trigger_.hasTrigTime = true;
resetTrigZoom();
} }
return; return;
} }
@@ -660,10 +841,24 @@ void App::onZoom(const std::string& json) {
for (int i = 0; i < kMaxPlotSlots; i++) { for (int i = 0; i < kMaxPlotSlots; i++) {
auto& zc = zoomCache_[i]; auto& zc = zoomCache_[i];
if (zc.pending && zc.reqId == resp.reqId) { if (zc.pending && zc.reqId == resp.reqId) {
/* Compute the actual time range from the returned data so the
* coverage check in PlotPanel can distinguish between "ring had
* data for the full window" and "ring only had partial data". */
double dataT0 = 1e300, dataT1 = -1e300;
bool anyData = false;
for (const auto& zs : resp.signals) {
for (double tv : zs.t) {
if (tv < dataT0) { dataT0 = tv; }
if (tv > dataT1) { dataT1 = tv; }
anyData = true;
}
}
if (anyData) {
zc.signals = std::move(resp.signals); zc.signals = std::move(resp.signals);
zc.t0 = zc.reqT0; zc.t0 = dataT0;
zc.t1 = zc.reqT1; zc.t1 = dataT1;
zc.valid = true; zc.valid = true;
}
zc.pending = false; zc.pending = false;
return; return;
} }
@@ -717,10 +912,22 @@ void App::onHistoryZoom(const std::string& json) {
for (int i = 0; i < kMaxPlotSlots; i++) { for (int i = 0; i < kMaxPlotSlots; i++) {
auto& hc = histZoomCache_[i]; auto& hc = histZoomCache_[i];
if (hc.pending && hc.reqId == resp.reqId) { if (hc.pending && hc.reqId == resp.reqId) {
/* Compute the actual time range from returned data. */
double dataT0 = 1e300, dataT1 = -1e300;
bool anyData = false;
for (const auto& zs : resp.signals) {
for (double tv : zs.t) {
if (tv < dataT0) { dataT0 = tv; }
if (tv > dataT1) { dataT1 = tv; }
anyData = true;
}
}
if (anyData) {
hc.signals = std::move(resp.signals); hc.signals = std::move(resp.signals);
hc.t0 = hc.reqT0; hc.t0 = dataT0;
hc.t1 = hc.reqT1; hc.t1 = dataT1;
hc.valid = true; hc.valid = true;
}
hc.pending = false; hc.pending = false;
return; return;
} }
+13 -3
View File
@@ -26,7 +26,7 @@ namespace StreamHubClient {
/** One signal as known to the client. */ /** One signal as known to the client. */
struct Signal { struct Signal {
SignalMeta meta; SignalMeta meta;
SignalBuffer buf{20000}; SignalBuffer buf{1000000};
ImVec4 color{0.4f, 0.8f, 1.0f, 1.0f}; ImVec4 color{0.4f, 0.8f, 1.0f, 1.0f};
float lineWidth = 1.5f; float lineWidth = 1.5f;
int marker = -1; /* ImPlotMarker (-1 = none) */ int marker = -1; /* ImPlotMarker (-1 = none) */
@@ -244,10 +244,15 @@ public:
/** @brief Hi-res history zoom cache (per plot, parallel to zoomCache_). */ /** @brief Hi-res history zoom cache (per plot, parallel to zoomCache_). */
PlotZoomCache& histZoomCache(int i) { return histZoomCache_[i]; } PlotZoomCache& histZoomCache(int i) { return histZoomCache_[i]; }
/* ---- Trigger zoom (per plot, used in trigger capture view) ----------- */
bool& trigZoomed(int i) { return trigZoomed_[i]; }
void resetTrigZoom() { for (int i = 0; i < kMaxPlotSlots; i++) trigZoomed_[i] = false; }
/** @brief True if the stats panel is open. */ /** @brief True if the stats panel is open. */
bool& showStats() { return showStats_; } bool& showStats() { return showStats_; }
bool& showAddSrc() { return showAddSrc_; } bool& showAddSrc() { return showAddSrc_; }
bool& showTrigBar() { return showTrigBar_; } bool& showTrigBar() { return showTrigBar_; }
bool& showHistBar() { return showHistBar_; }
/** @brief Find source index by id (-1 if not found). */ /** @brief Find source index by id (-1 if not found). */
int findSource(const std::string& id) const; int findSource(const std::string& id) const;
@@ -270,8 +275,8 @@ private:
void onMaxPointsUpdated(const std::string& json); void onMaxPointsUpdated(const std::string& json);
/* ---- Rendering ------------------------------------------------------ */ /* ---- Rendering ------------------------------------------------------ */
void renderMenuBar();
void renderToolbar(); void renderToolbar();
void renderHistoryBar();
void renderTriggerBar(); void renderTriggerBar();
void renderSidebar(); void renderSidebar();
void renderPlotGrid(); void renderPlotGrid();
@@ -285,7 +290,7 @@ private:
TriggerState trigger_; TriggerState trigger_;
PlotLayout layout_ = PlotLayout::kLayout1x1; PlotLayout layout_ = PlotLayout::kLayout1x1;
uint32_t maxPoints_ = 20000u; uint32_t maxPoints_ = 1000000u;
bool globalPaused_ = false; bool globalPaused_ = false;
/* Plot slots: one vector of assignments per plot panel */ /* Plot slots: one vector of assignments per plot panel */
@@ -304,6 +309,9 @@ private:
double cursorA_ = 0.0; double cursorA_ = 0.0;
double cursorB_ = 0.0; double cursorB_ = 0.0;
/* Trigger zoom per plot (capture view zoom state) */
bool trigZoomed_[kMaxPlotSlots] = {};
/* Frozen view per plot (valid while the plot is paused) */ /* Frozen view per plot (valid while the plot is paused) */
PlotSnapshot plotSnap_[kMaxPlotSlots]; PlotSnapshot plotSnap_[kMaxPlotSlots];
@@ -315,11 +323,13 @@ private:
/* History info from the hub */ /* History info from the hub */
HistoryInfoMsg historyInfo_; HistoryInfoMsg historyInfo_;
double lastHistInfoReq_ = 0.0;
/* UI state */ /* UI state */
bool showStats_ = false; bool showStats_ = false;
bool showAddSrc_ = false; bool showAddSrc_ = false;
bool showTrigBar_ = false; bool showTrigBar_ = false;
bool showHistBar_ = false;
bool sidebarOpen_ = true; bool sidebarOpen_ = true;
/* Connection fields (populated by init(), editable in menu bar) */ /* Connection fields (populated by init(), editable in menu bar) */
+119 -44
View File
@@ -39,8 +39,10 @@ struct DragPayload { int srcIdx; int sigIdx; };
static const int kPlotSlotsMax = 8; /* mirrors App::kMaxPlotSlots */ static const int kPlotSlotsMax = 8; /* mirrors App::kMaxPlotSlots */
/* Live windows at or below this width refresh via hub hi-res zoom fetches /* Live windows at or below this width refresh via hub hi-res zoom fetches
* instead of (in addition to) the decimated push stream. */ * instead of (in addition to) the decimated push stream. Wide windows
static const double kLiveHiResMaxWin = 2.0; /* seconds */ * benefit too: LTTB on the server ring gives much better coverage than
* the client's accumulated push-stream data. */
static const double kLiveHiResMaxWin = 600.0; /* seconds (effectively always) */
/* ── Helpers ─────────────────────────────────────────────────────────────── */ /* ── Helpers ─────────────────────────────────────────────────────────────── */
@@ -198,18 +200,30 @@ void RenderPlotPanel(App& app, int plotIdx, bool& paused) {
(zc.t1 - zc.t0) >= app.windowSec() * 0.9 && (zc.t1 - zc.t0) >= app.windowSec() * 0.9 &&
(wallNow - zc.t1) < 3.0; (wallNow - zc.t1) < 3.0;
const bool useZoomData = !trigView && zc.valid && const bool useZoomData = !trigView && !paused && zc.valid &&
(liveHiRes || (liveHiRes ||
(!live && (!live &&
zc.t0 <= app.plotXMin(plotIdx) + 1e-9 && zc.t0 <= app.plotXMin(plotIdx) + 1e-9 &&
zc.t1 >= app.plotXMax(plotIdx) - 1e-9)); zc.t1 >= app.plotXMax(plotIdx) - 1e-9));
/* History zoom cache (disk-backed, for ranges beyond in-memory ring) */ /* History zoom cache (disk-backed, for ranges beyond in-memory ring).
* Zoom responses carry the *requested* t0/t1 (not actual data bounds),
* so a zoom cache can report coverage even when the ring had no data.
* Prefer history over zoom when the in-memory ring cannot span the full
* visible window — that is the typical history-browse case. */
auto& hc = app.histZoomCache(plotIdx); auto& hc = app.histZoomCache(plotIdx);
const bool useHistData = !trigView && !live && !useZoomData && const bool haveHistCover = hc.valid &&
hc.valid &&
hc.t0 <= app.plotXMin(plotIdx) + 1e-9 && hc.t0 <= app.plotXMin(plotIdx) + 1e-9 &&
hc.t1 >= app.plotXMax(plotIdx) - 1e-9; hc.t1 >= app.plotXMax(plotIdx) - 1e-9;
bool useHistData = !trigView && !paused && !live && haveHistCover;
if (useHistData) {
/* Check that at least one signal has actual data points */
bool anyData = false;
for (const auto& hs : hc.signals) {
if (!hs.t.empty()) { anyData = true; break; }
}
if (!anyData) { useHistData = false; }
}
/* Fill tStore/vStore[si] from the frozen view (paused) or the live ring */ /* Fill tStore/vStore[si] from the frozen view (paused) or the live ring */
auto readBase = [&](size_t si, const Signal& sig, const std::string& key) { auto readBase = [&](size_t si, const Signal& sig, const std::string& key) {
@@ -369,8 +383,8 @@ void RenderPlotPanel(App& app, int plotIdx, bool& paused) {
ImGui::PopStyleColor(2); ImGui::PopStyleColor(2);
} }
/* Back / Fit (zoom history) — only meaningful when not live */ /* Back / Fit / Reset (zoom history) */
if (!live && !trigView) { if (!live || (trigView && app.trigZoomed(plotIdx))) {
ImGui::SameLine(); ImGui::SameLine();
auto& hist = app.zoomHist(plotIdx); auto& hist = app.zoomHist(plotIdx);
if (hist.empty()) { ImGui::BeginDisabled(); } if (hist.empty()) { ImGui::BeginDisabled(); }
@@ -380,6 +394,13 @@ void RenderPlotPanel(App& app, int plotIdx, bool& paused) {
} }
if (hist.empty()) { ImGui::EndDisabled(); } if (hist.empty()) { ImGui::EndDisabled(); }
ImGui::SameLine(); ImGui::SameLine();
if (trigView) {
/* Reset to full capture window */
if (ImGui::SmallButton(ICON_FA_EXPAND " Reset##zr")) {
app.trigZoomed(plotIdx) = false;
app.zoomHist(plotIdx).clear();
}
} else {
if (ImGui::SmallButton(ICON_FA_EXPAND " Fit##zf")) { if (ImGui::SmallButton(ICON_FA_EXPAND " Fit##zf")) {
double mn = 1e300, mx = -1e300; double mn = 1e300, mx = -1e300;
for (size_t si = 0; si < slots.size(); si++) { for (size_t si = 0; si < slots.size(); si++) {
@@ -393,6 +414,7 @@ void RenderPlotPanel(App& app, int plotIdx, bool& paused) {
} }
} }
} }
}
/* History indicator */ /* History indicator */
if (useHistData) { if (useHistData) {
@@ -401,13 +423,23 @@ void RenderPlotPanel(App& app, int plotIdx, bool& paused) {
ICON_FA_CLOCK_ROTATE_LEFT " HIST"); ICON_FA_CLOCK_ROTATE_LEFT " HIST");
} }
/* Norm mode + cursors toggles */ /* Norm/Dig/Mix mode compact toggle buttons matching SmallButton height */
ImGui::SameLine(); ImGui::SameLine();
{ {
static const char* kVModes[] = {"Norm", "Dig", "Mix"}; static const char* kVLabels[] = {"N", "D", "M"};
ImGui::SetNextItemWidth(58.f); static const char* kVTooltips[] = {"Normal", "Digital", "Mixed"};
char vmId[16]; snprintf(vmId, sizeof(vmId), "##vm%d", plotIdx); for (int vm = 0; vm < 3; vm++) {
ImGui::Combo(vmId, &vMode, kVModes, 3); char vmId[16]; snprintf(vmId, sizeof(vmId), "%s##vm%d_%d", kVLabels[vm], plotIdx, vm);
bool sel = (vMode == vm);
if (sel) {
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.537f,0.706f,0.980f,0.4f));
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.537f,0.706f,0.980f,1.f));
}
if (ImGui::SmallButton(vmId)) { vMode = vm; }
if (sel) { ImGui::PopStyleColor(2); }
if (ImGui::IsItemHovered()) { ImGui::SetTooltip("%s", kVTooltips[vm]); }
if (vm < 2) { ImGui::SameLine(0.f, 1.f); }
}
} }
/* ── VScale toolbar (shown when an active signal is selected) ───────── */ /* ── VScale toolbar (shown when an active signal is selected) ───────── */
@@ -500,10 +532,17 @@ void RenderPlotPanel(App& app, int plotIdx, bool& paused) {
/* Y axis always ±4 div space */ /* Y axis always ±4 div space */
ImPlot::SetupAxisLimits(ImAxis_Y1, -4.05, 4.05, ImGuiCond_Always); ImPlot::SetupAxisLimits(ImAxis_Y1, -4.05, 4.05, ImGuiCond_Always);
/* X axis: trig view → capture window; live → wall clock; else stored */ /* X axis: trig view → capture window (zoomable); live → wall clock; else stored */
double xMin, xMax; double xMin, xMax;
bool& trigZm = app.trigZoomed(plotIdx);
if (trigView) { if (trigView) {
xMin = -cap->preSec; xMax = cap->postSec; if (trigZm) {
xMin = app.plotXMin(plotIdx);
xMax = app.plotXMax(plotIdx);
} else {
xMin = -cap->preSec;
xMax = cap->postSec;
}
} else if (live && !paused) { } else if (live && !paused) {
if (liveHiRes) { if (liveHiRes) {
/* Anchor to the latest fetched hi-res slice so the trace /* Anchor to the latest fetched hi-res slice so the trace
@@ -574,24 +613,24 @@ void RenderPlotPanel(App& app, int plotIdx, bool& paused) {
/* Zoom-history debounce: coalesce rapid scroll steps into one entry */ /* Zoom-history debounce: coalesce rapid scroll steps into one entry */
static double lastHistPush[kPlotSlotsMax] = {}; static double lastHistPush[kPlotSlotsMax] = {};
if (overPlot && !trigView) { if (overPlot) {
ImGuiIO& io2 = ImGui::GetIO(); ImGuiIO& io2 = ImGui::GetIO();
const float wheel = io2.MouseWheel; const float wheel = io2.MouseWheel;
const bool ctrl = io2.KeyCtrl; const bool ctrl = io2.KeyCtrl;
const bool shift = io2.KeyShift; const bool shift = io2.KeyShift;
const double now = ImGui::GetTime(); const double now = ImGui::GetTime();
if (wheel != 0.f) { /* Helper: enter zoomed mode for trigger view (seed from capture window) */
/* Zoom factor: scroll up = zoom in (×0.8), down = zoom out (×1.25) */ auto enterTrigZoom = [&]() {
const double zoomIn = 0.8; if (trigView && !trigZm) {
const double zoomOut = 1.25; app.setPlotX(plotIdx, -cap->preSec, cap->postSec);
double factor = (wheel > 0.f) ? zoomIn : zoomOut; trigZm = true;
}
};
if (ctrl) { /* Helper: X-zoom the stored range by factor around center */
/* ── X zoom ─────────────────────────────────────────── */ auto xZoomStored = [&](double factor) {
if (live) { if (trigView) { enterTrigZoom(); }
app.setWindowSec(app.windowSec() * factor);
} else {
if (now - lastHistPush[plotIdx] > 0.6) { if (now - lastHistPush[plotIdx] > 0.6) {
app.pushZoomHist(plotIdx); app.pushZoomHist(plotIdx);
lastHistPush[plotIdx] = now; lastHistPush[plotIdx] = now;
@@ -599,18 +638,29 @@ void RenderPlotPanel(App& app, int plotIdx, bool& paused) {
double cx = (app.plotXMin(plotIdx) + app.plotXMax(plotIdx)) * 0.5; double cx = (app.plotXMin(plotIdx) + app.plotXMax(plotIdx)) * 0.5;
double half = (app.plotXMax(plotIdx) - app.plotXMin(plotIdx)) * 0.5 * factor; double half = (app.plotXMax(plotIdx) - app.plotXMin(plotIdx)) * 0.5 * factor;
app.setPlotX(plotIdx, cx - half, cx + half); app.setPlotX(plotIdx, cx - half, cx + half);
};
if (wheel != 0.f) {
const double zoomIn = 0.8;
const double zoomOut = 1.25;
double factor = (wheel > 0.f) ? zoomIn : zoomOut;
if (ctrl) {
/* ── X zoom ─────────────────────────────────────────── */
if (!trigView && live) {
app.setWindowSec(app.windowSec() * factor);
} else {
xZoomStored(factor);
} }
} else if (shift) { } else if (shift) {
/* ── Y offset of active signal ───────────────────────── */ /* ── Y offset of active signal ───────────────────────── */
if (actSlot >= 0 && actSlot < (int)slots.size()) { if (actSlot >= 0 && actSlot < (int)slots.size()) {
auto& a = slots[actSlot]; auto& a = slots[actSlot];
/* Switch to manual so position is retained */
if (a.vs.mode != 2) { if (a.vs.mode != 2) {
a.vs.divValue = std::max(a.vs.resolvedDiv, 1e-30); a.vs.divValue = std::max(a.vs.resolvedDiv, 1e-30);
a.vs.offset = a.vs.resolvedOffset; a.vs.offset = a.vs.resolvedOffset;
a.vs.mode = 2; a.vs.mode = 2;
} }
/* Each scroll step moves by 0.5 screen divisions */
a.vs.screenPos += (wheel > 0.f) ? 0.5 : -0.5; a.vs.screenPos += (wheel > 0.f) ? 0.5 : -0.5;
} }
} else { } else {
@@ -624,27 +674,21 @@ void RenderPlotPanel(App& app, int plotIdx, bool& paused) {
} }
a.vs.divValue = std::max(a.vs.divValue * factor, 1e-30); a.vs.divValue = std::max(a.vs.divValue * factor, 1e-30);
} else { } else {
/* No active signal: plain scroll falls back to X zoom /* No active signal: plain scroll X zoom */
* so the wheel always does something useful. */ if (!trigView && live) {
if (live) {
app.setWindowSec(app.windowSec() * factor); app.setWindowSec(app.windowSec() * factor);
} else { } else {
if (now - lastHistPush[plotIdx] > 0.6) { xZoomStored(factor);
app.pushZoomHist(plotIdx);
lastHistPush[plotIdx] = now;
}
double cx = (app.plotXMin(plotIdx) + app.plotXMax(plotIdx)) * 0.5;
double half = (app.plotXMax(plotIdx) - app.plotXMin(plotIdx)) * 0.5 * factor;
app.setPlotX(plotIdx, cx - half, cx + half);
} }
} }
} }
} }
/* Right-drag → X pan (non-live). Transition live→non-live on drag start. */ /* Right-drag → X pan. Transition live→non-live on drag start;
* in trigger view, enter trigger-zoom mode. */
if (ImGui::IsMouseDragging(ImGuiMouseButton_Right)) { if (ImGui::IsMouseDragging(ImGuiMouseButton_Right)) {
if (live) { if (trigView) { enterTrigZoom(); }
/* Seed stored range from current live window */ if (!trigView && live) {
app.initPlotX(plotIdx, wallNow); app.initPlotX(plotIdx, wallNow);
live = false; live = false;
lastHistPush[plotIdx] = now; lastHistPush[plotIdx] = now;
@@ -662,8 +706,8 @@ void RenderPlotPanel(App& app, int plotIdx, bool& paused) {
} }
} }
/* ── Hi-res WS zoom requests ────────────────────────────────────── */ /* ── Hi-res WS zoom requests (suppressed while paused) ──────────── */
if (!trigView) { if (!trigView && !paused) {
std::string csv; std::string csv;
for (const auto& a : slots) { for (const auto& a : slots) {
std::string k = app.slotKey(a); std::string k = app.slotKey(a);
@@ -714,6 +758,28 @@ void RenderPlotPanel(App& app, int plotIdx, bool& paused) {
} }
} }
/* ── Clip data to visible X range before LTTB ─────────────────── *
* The ring buffer may hold far more data than the visible window.
* LTTB must operate only on the visible slice so it picks the best
* 2400 points for what's actually on screen. */
if (xMax > xMin) {
for (size_t si = 0; si < slots.size(); si++) {
auto& tv = tStore[si];
auto& vv = vStore[si];
if (tv.empty()) continue;
auto lo = std::lower_bound(tv.begin(), tv.end(), xMin);
auto hi = std::upper_bound(lo, tv.end(), xMax);
if (lo != tv.begin()) --lo; /* keep one sample before window */
if (hi != tv.end()) ++hi; /* keep one sample after window */
size_t i0 = (size_t)(lo - tv.begin());
size_t i1 = (size_t)(hi - tv.begin());
if (i0 > 0 || i1 < tv.size()) {
tv = std::vector<double>(tv.begin() + i0, tv.begin() + i1);
vv = std::vector<double>(vv.begin() + i0, vv.begin() + i1);
}
}
}
/* ── Plot each signal ──────────────────────────────────────────── */ /* ── Plot each signal ──────────────────────────────────────────── */
static const size_t kMaxPush = 2400; static const size_t kMaxPush = 2400;
static thread_local std::vector<double> tDec, vDec, vNorm; static thread_local std::vector<double> tDec, vDec, vNorm;
@@ -762,8 +828,17 @@ void RenderPlotPanel(App& app, int plotIdx, bool& paused) {
1.5f, ImPlotDragToolFlags_NoInputs); 1.5f, ImPlotDragToolFlags_NoInputs);
} }
/* Cursors A/B — global: same position on every plot, drag anywhere */ /* Cursors A/B — global: same position on every plot, drag anywhere.
* Re-seed to visible range if both cursors are outside the view
* (e.g. switching between live and trigger mode). */
if (app.cursorsOn()) { if (app.cursorsOn()) {
bool aBeyond = (app.cursorA() < xMin || app.cursorA() > xMax);
bool bBeyond = (app.cursorB() < xMin || app.cursorB() > xMax);
if (aBeyond && bBeyond) {
double range = xMax - xMin;
app.cursorA() = xMin + range * 0.25;
app.cursorB() = xMin + range * 0.75;
}
static const ImVec4 kCursA{0.980f,0.702f,0.529f,0.9f}; static const ImVec4 kCursA{0.980f,0.702f,0.529f,0.9f};
static const ImVec4 kCursB{0.796f,0.651f,0.969f,0.9f}; static const ImVec4 kCursB{0.796f,0.651f,0.969f,0.9f};
ImPlot::DragLineX(910, &app.cursorA(), kCursA, 1.2f); ImPlot::DragLineX(910, &app.cursorA(), kCursA, 1.2f);
+2 -3
View File
@@ -553,9 +553,8 @@ bool ParseHistoryInfo(const std::string& json, HistoryInfoMsg& out) {
out.signals.clear(); out.signals.clear();
out.enabled = false; out.enabled = false;
char tmp[16] = ""; /* "enabled" is a JSON boolean (unquoted true/false), not a string */
jsonGetStr(json.c_str(), "enabled", tmp, sizeof(tmp)); out.enabled = (strstr(json.c_str(), "\"enabled\":true") != nullptr);
out.enabled = (strcmp(tmp, "true") == 0);
double df = 0.0; double df = 0.0;
jsonGetDouble(json.c_str(), "durationHours", df); jsonGetDouble(json.c_str(), "durationHours", df);
+12
View File
@@ -100,6 +100,18 @@ void RenderSourcePanel(App& app) {
} }
ImGui::PopStyleVar(); ImGui::PopStyleVar();
/* ── Add / Save sources (inline at the bottom of the sidebar) ──────── */
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
if (ImGui::Button(ICON_FA_PLUS " Add Source", ImVec2(-1.f, 0.f))) {
app.showAddSrc() = true;
}
if (ImGui::Button(ICON_FA_FLOPPY_DISK " Save Sources", ImVec2(-1.f, 0.f))) {
app.ws().sendText(BuildSaveSources());
}
} }
} /* namespace StreamHubClient */ } /* namespace StreamHubClient */
+29 -44
View File
@@ -5,6 +5,9 @@
* The trigger runs in the C++ StreamHub: this panel only edits the config * The trigger runs in the C++ StreamHub: this panel only edits the config
* (signal/edge/threshold/window/pre%/mode), sends setTrigger + arm/disarm/ * (signal/edge/threshold/window/pre%/mode), sends setTrigger + arm/disarm/
* rearm/trigStop commands, and reflects the hub triggerState broadcasts. * rearm/trigStop commands, and reflects the hub triggerState broadcasts.
*
* Single-row compact layout: all controls fit on one line on a ≥1280 px window;
* ImGui wraps naturally on narrower screens without a scrollbar.
*/ */
#include "TriggerPanel.h" #include "TriggerPanel.h"
@@ -22,12 +25,10 @@ namespace StreamHubClient {
/* Window presets (mirrors the web UI: 100 µs .. 10 s) */ /* Window presets (mirrors the web UI: 100 µs .. 10 s) */
static const double kWinVals[] = {1e-4, 1e-3, 1e-2, 1e-1, 1.0, 10.0}; static const double kWinVals[] = {1e-4, 1e-3, 1e-2, 1e-1, 1.0, 10.0};
static const char* kWinLabels[] = {"100 µs", "1 ms", "10 ms", "100 ms", "1 s", "10 s"}; static const char* kWinLabels[] = {"100 us", "1 ms", "10 ms", "100 ms", "1 s", "10 s"};
static const int kNumWins = 6; static const int kNumWins = 6;
static const char* kEdgeLabels[] = {ICON_FA_ARROW_TREND_UP " Rising", static const char* kEdgeLabels[] = {"Rising", "Falling", "Both"};
ICON_FA_ARROW_TREND_DOWN " Falling",
ICON_FA_ARROWS_UP_DOWN " Both"};
static const char* kEdgeWire[] = {"rising", "falling", "both"}; static const char* kEdgeWire[] = {"rising", "falling", "both"};
/* Send the current config to the hub. */ /* Send the current config to the hub. */
@@ -44,20 +45,14 @@ void RenderTriggerPanel(App& app) {
auto& trig = app.trigger(); auto& trig = app.trigger();
auto& sources = app.sources(); auto& sources = app.sources();
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.15f, 0.15f, 0.20f, 1.f)); ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(4.f, 4.f));
ImGui::BeginChild("##trigbar", ImVec2(0.f, 90.f), true);
ImGui::PopStyleColor();
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(6.f, 4.f));
bool cfgChanged = false; bool cfgChanged = false;
/* ── Signal selector (full keys "src:sig") ─────────────────────────── * /* ── Signal selector ──────────────────────────────────────────────── */
* Vector signals are flattened time-series on the hub: one key per ImGui::SetNextItemWidth(150.f);
* signal; the trigger engine checks every decoded sample of it. */ if (ImGui::BeginCombo("##trigsig", trig.signalKey.empty()
ImGui::SetNextItemWidth(180.f); ? "<signal>" : trig.signalKey.c_str())) {
if (ImGui::BeginCombo("Signal##trig", trig.signalKey.empty()
? "<select>" : trig.signalKey.c_str())) {
for (const auto& src : sources) { for (const auto& src : sources) {
for (const auto& sig : src.signals) { for (const auto& sig : src.signals) {
char key[192]; char key[192];
@@ -76,14 +71,14 @@ void RenderTriggerPanel(App& app) {
ImGui::SameLine(); ImGui::SameLine();
/* ── Edge ─────────────────────────────────────────────────────────── */ /* ── Edge ─────────────────────────────────────────────────────────── */
ImGui::SetNextItemWidth(95.f); ImGui::SetNextItemWidth(70.f);
if (ImGui::Combo("Edge##trig", &trig.edge, kEdgeLabels, 3)) { if (ImGui::Combo("##trigedge", &trig.edge, kEdgeLabels, 3)) {
cfgChanged = true; cfgChanged = true;
} }
ImGui::SameLine(); ImGui::SameLine();
/* ── Threshold ───────────────────────────────────────────────────── */ /* ── Threshold ───────────────────────────────────────────────────── */
ImGui::SetNextItemWidth(80.f); ImGui::SetNextItemWidth(65.f);
if (ImGui::InputDouble("Thr##trig", &trig.threshold, 0.0, 0.0, "%.4g", if (ImGui::InputDouble("Thr##trig", &trig.threshold, 0.0, 0.0, "%.4g",
ImGuiInputTextFlags_EnterReturnsTrue)) { ImGuiInputTextFlags_EnterReturnsTrue)) {
cfgChanged = true; cfgChanged = true;
@@ -97,7 +92,7 @@ void RenderTriggerPanel(App& app) {
winIdx = w; break; winIdx = w; break;
} }
} }
ImGui::SetNextItemWidth(85.f); ImGui::SetNextItemWidth(75.f);
if (ImGui::Combo("Win##trig", &winIdx, kWinLabels, kNumWins)) { if (ImGui::Combo("Win##trig", &winIdx, kWinLabels, kNumWins)) {
trig.windowSec = kWinVals[winIdx]; trig.windowSec = kWinVals[winIdx];
cfgChanged = true; cfgChanged = true;
@@ -105,24 +100,21 @@ void RenderTriggerPanel(App& app) {
ImGui::SameLine(); ImGui::SameLine();
/* ── Pre-trigger % ───────────────────────────────────────────────── */ /* ── Pre-trigger % ───────────────────────────────────────────────── */
ImGui::SetNextItemWidth(100.f); ImGui::SetNextItemWidth(70.f);
float preP = static_cast<float>(trig.prePercent); float preP = static_cast<float>(trig.prePercent);
if (ImGui::SliderFloat("Pre%##trig", &preP, 0.f, 100.f, "%.0f%%")) { if (ImGui::SliderFloat("Pre##trig", &preP, 0.f, 100.f, "%.0f%%")) {
trig.prePercent = static_cast<double>(preP); trig.prePercent = static_cast<double>(preP);
} }
if (ImGui::IsItemDeactivatedAfterEdit()) { cfgChanged = true; } if (ImGui::IsItemDeactivatedAfterEdit()) { cfgChanged = true; }
ImGui::SameLine(); ImGui::SameLine();
/* ── Mode ────────────────────────────────────────────────────────── */ /* ── Mode ────────────────────────────────────────────────────────── */
ImGui::TextUnformatted("Mode:");
ImGui::SameLine();
int singleInt = trig.single ? 1 : 0; int singleInt = trig.single ? 1 : 0;
if (ImGui::RadioButton("Normal", &singleInt, 0)) { cfgChanged = true; } if (ImGui::RadioButton("Norm##trig", &singleInt, 0)) { cfgChanged = true; }
ImGui::SameLine(); ImGui::SameLine();
if (ImGui::RadioButton("Single", &singleInt, 1)) { cfgChanged = true; } if (ImGui::RadioButton("1x##trig", &singleInt, 1)) { cfgChanged = true; }
trig.single = (singleInt != 0); trig.single = (singleInt != 0);
ImGui::SameLine(0.f, 10.f);
ImGui::NewLine();
/* ── Status badge ────────────────────────────────────────────────── */ /* ── Status badge ────────────────────────────────────────────────── */
ImVec4 badgeColor; ImVec4 badgeColor;
@@ -132,44 +124,38 @@ void RenderTriggerPanel(App& app) {
else if (trig.status == "triggered") { badgeColor = ImVec4(0.2f,0.9f,0.3f,1.f); badgeText = "TRIGGERED"; } else if (trig.status == "triggered") { badgeColor = ImVec4(0.2f,0.9f,0.3f,1.f); badgeText = "TRIGGERED"; }
else { badgeColor = ImVec4(0.6f,0.6f,0.6f,1.f); badgeText = "IDLE"; } else { badgeColor = ImVec4(0.6f,0.6f,0.6f,1.f); badgeText = "IDLE"; }
ImGui::TextColored(badgeColor, "[%s]", badgeText); ImGui::TextColored(badgeColor, "[%s]", badgeText);
if (trig.hasTrigTime &&
(trig.status == "collecting" || trig.status == "triggered")) {
ImGui::SameLine();
ImGui::TextDisabled("t=%.6f", trig.trigTime);
}
ImGui::SameLine(); ImGui::SameLine();
/* ── Arm / Disarm / Rearm / Stop ─────────────────────────────────── */ /* ── Arm / Disarm / Stop ────────────────────────────────────────── */
bool canArm = !trig.signalKey.empty(); bool canArm = !trig.signalKey.empty();
if (!canArm) { ImGui::BeginDisabled(); } if (!canArm) { ImGui::BeginDisabled(); }
if (ImGui::Button(ICON_FA_BOLT " Arm")) { if (ImGui::SmallButton(ICON_FA_BOLT " Arm")) {
sendTrigConfig(app); sendTrigConfig(app);
app.ws().sendText(BuildArm()); app.ws().sendText(BuildArm());
} }
if (!canArm) { ImGui::EndDisabled(); } if (!canArm) { ImGui::EndDisabled(); }
ImGui::SameLine(); ImGui::SameLine();
if (ImGui::Button("Disarm")) { if (ImGui::SmallButton("Disarm")) {
app.ws().sendText(BuildDisarm()); app.ws().sendText(BuildDisarm());
} }
ImGui::SameLine(); ImGui::SameLine();
if (trig.single && trig.status == "triggered") {
if (ImGui::Button("Rearm")) {
app.ws().sendText(BuildRearm());
}
ImGui::SameLine();
}
if (!trig.single) { if (!trig.single) {
if (ImGui::Button(trig.stopped ? ICON_FA_PLAY " Run" : ICON_FA_STOP " Stop")) { if (ImGui::SmallButton(trig.stopped ? ICON_FA_PLAY " Run" : ICON_FA_STOP " Stop")) {
trig.stopped = !trig.stopped; trig.stopped = !trig.stopped;
app.ws().sendText(BuildTrigStop(trig.stopped)); app.ws().sendText(BuildTrigStop(trig.stopped));
} }
ImGui::SameLine(); ImGui::SameLine();
} }
/* Trigger time readout (inline) */
if (trig.hasTrigTime &&
(trig.status == "collecting" || trig.status == "triggered")) {
ImGui::TextDisabled("t=%.6f", trig.trigTime);
} else {
ImGui::NewLine(); ImGui::NewLine();
}
/* Config edits while armed take effect immediately on the hub */ /* Config edits while armed take effect immediately on the hub */
if (cfgChanged && !trig.signalKey.empty()) { if (cfgChanged && !trig.signalKey.empty()) {
@@ -177,7 +163,6 @@ void RenderTriggerPanel(App& app) {
} }
ImGui::PopStyleVar(); ImGui::PopStyleVar();
ImGui::EndChild();
ImGui::Separator(); ImGui::Separator();
} }
Binary file not shown.
+4
View File
@@ -15,7 +15,9 @@ apps:
$(MAKE) -C Source/Applications/StreamHub -f Makefile.gcc $(MAKE) -C Source/Applications/StreamHub -f Makefile.gcc
core: core:
$(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/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
@@ -29,7 +31,9 @@ test:
clean: clean:
$(MAKE) -C Source/Applications/StreamHub -f Makefile.gcc clean $(MAKE) -C Source/Applications/StreamHub -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/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
@@ -5,6 +5,7 @@
#include "HistoryWriter.h" #include "HistoryWriter.h"
#include "AdvancedErrorManagement.h" #include "AdvancedErrorManagement.h"
#include "UDPSProtocol.h"
#include <stdio.h> #include <stdio.h>
#include <string.h> #include <string.h>
@@ -129,8 +130,8 @@ void HistoryWriter::OnSourceConfigured(uint32 sessionIdx, const char *sourceId,
MARTe::UDPSSignalDescriptor desc; MARTe::UDPSSignalDescriptor desc;
if (!sess.GetSignalDescriptor(s, desc)) { continue; } if (!sess.GetSignalDescriptor(s, desc)) { continue; }
/* Skip time-only signals (type uint64, typically TimeArray) */ /* Skip time-reference signals (uint64 type, typically TimeArray) */
if (desc.typeCode == 13u) { continue; } /* uint64 = 13 in UDPS */ if (desc.typeCode == MARTe::UDPS_TYPECODE_UINT64) { continue; }
float64 estRate = static_cast<float64>(desc.samplingRate); float64 estRate = static_cast<float64>(desc.samplingRate);
if (estRate <= 0.0) { estRate = 1000.0; } /* fallback */ if (estRate <= 0.0) { estRate = 1000.0; } /* fallback */
@@ -490,8 +491,9 @@ void HistoryWriter::AppendInfoJSON(char *&buf, uint32 &off, uint32 &cap) const {
for (uint32 s = 0u; s < kHistMaxSignals; s++) { for (uint32 s = 0u; s < kHistMaxSignals; s++) {
if (!fileActive_[i][s]) { continue; } if (!fileActive_[i][s]) { continue; }
const SignalFile &sf = files_[i][s]; const SignalFile &sf = files_[i][s];
if (sf.count == 0u) { continue; } /* Report even when count==0 — the file exists but hasn't received
* data yet (or was just created). Clients need the entry to enable
* history UI; t0/t1 == 0 signals "still priming". */
HistJsonAppendf(buf, off, cap, HistJsonAppendf(buf, off, cap,
"%s\"%s:%s\":{\"t0\":%.17g,\"t1\":%.17g,\"count\":%u,\"capacity\":%u}", "%s\"%s:%s\":{\"t0\":%.17g,\"t1\":%.17g,\"count\":%u,\"capacity\":%u}",
(first ? "" : ","), (first ? "" : ","),
@@ -42,6 +42,12 @@ public:
*/ */
void Write(float64 t, float64 v); void Write(float64 t, float64 v);
/**
* @brief Write @p n (t, v) pairs in a single lock acquisition.
* Much faster than calling Write() n times for large batches.
*/
void WriteBatch(const float64 *t, const float64 *v, uint32 n);
/** /**
* @brief Copy the last (oldest-to-newest) min(n, count) points into tOut/vOut. * @brief Copy the last (oldest-to-newest) min(n, count) points into tOut/vOut.
* @return Number of points actually written. * @return Number of points actually written.
@@ -144,6 +150,22 @@ inline void SignalRingBuffer::Write(float64 t, float64 v) {
mutex.FastUnLock(); mutex.FastUnLock();
} }
inline void SignalRingBuffer::WriteBatch(const float64 *t, const float64 *v,
uint32 n) {
if (n == 0u) { return; }
(void) mutex.FastLock();
if (capacity > 0u) {
for (uint32 i = 0u; i < n; i++) {
tBuf[head] = t[i];
vBuf[head] = v[i];
head = (head + 1u) % capacity;
if (count < capacity) { count++; }
}
totalWritten += static_cast<MARTe::uint64>(n);
}
mutex.FastUnLock();
}
inline uint32 SignalRingBuffer::Read(float64 *tOut, float64 *vOut, uint32 n) const { inline uint32 SignalRingBuffer::Read(float64 *tOut, float64 *vOut, uint32 n) const {
if ((capacity == 0u) || (count == 0u)) { return 0u; } if ((capacity == 0u) || (count == 0u)) { return 0u; }
(void) mutex.FastLock(); (void) mutex.FastLock();
+27 -5
View File
@@ -255,13 +255,27 @@ bool StreamHub::Run() {
PushStats(); PushStats();
} }
/* History: flush headers periodically */ /* History: flush headers at the configured interval, then re-broadcast
* historyInfo so new clients (or clients that saw empty signal lists
* before the first data was written) pick up the current state. */
if (history_.IsEnabled()) { if (history_.IsEnabled()) {
uint32 histFlushDiv = history_.Decimation() > 0u uint32 flushIntervalTicks = pushRateHz_ * 5u; /* default 5 s */
? pushRateHz_ * 5u : pushRateHz_ * 5u; /* every 5s */ if (pushRateHz_ > 0u) {
if (histFlushDiv == 0u) { histFlushDiv = 1u; } flushIntervalTicks = pushRateHz_ * 5u;
if ((tickCount_ % histFlushDiv) == 0u) { }
if (flushIntervalTicks == 0u) { flushIntervalTicks = 1u; }
if ((tickCount_ % flushIntervalTicks) == 0u) {
history_.FlushHeaders(); history_.FlushHeaders();
/* Re-broadcast so clients see updated signal counts and
* time ranges after the first batch of data is written. */
uint32 cap2 = 8192u;
char *hbuf = new char[cap2];
uint32 hoff = 0u;
JsonAppendf(hbuf, hoff, cap2, "{\"type\":\"historyInfo\",");
history_.AppendInfoJSON(hbuf, hoff, cap2);
JsonAppendf(hbuf, hoff, cap2, "}");
wsServer_.BroadcastText(hbuf, hoff);
delete[] hbuf;
} }
} }
@@ -648,6 +662,14 @@ void StreamHub::OnWSClientConnected() {
wsServer_.BroadcastText(hbuf, hoff); wsServer_.BroadcastText(hbuf, hoff);
delete[] hbuf; delete[] hbuf;
} }
/* Inform the new client about the current MaxPoints setting. */
{
char mp[64];
int n = snprintf(mp, sizeof(mp),
"{\"type\":\"maxPointsUpdated\",\"maxPoints\":%u}", maxPoints_);
if (n > 0) { wsServer_.BroadcastText(mp, static_cast<uint32>(n)); }
}
} }
void StreamHub::OnWSClientDisconnected() { void StreamHub::OnWSClientDisconnected() {
@@ -63,7 +63,10 @@ UDPSourceSession::UDPSourceSession()
statLastRxTicks_(0u), statLastRxTicks_(0u),
statFragCount_(0u), statFragCount_(0u),
statByteCount_(0u), statByteCount_(0u),
hrtFreq_(static_cast<float64>(MARTe::HighResolutionTimer::Frequency())),
timeScratch_(static_cast<float64 *>(0)), timeScratch_(static_cast<float64 *>(0)),
valScratch_(static_cast<float64 *>(0)),
tsBatchScratch_(static_cast<float64 *>(0)),
timeScratchLen_(0u), timeScratchLen_(0u),
trigEngine_(static_cast<TriggerEngine *>(0)), trigEngine_(static_cast<TriggerEngine *>(0)),
trigEpochSeen_(0xFFFFFFFFu), trigEpochSeen_(0xFFFFFFFFu),
@@ -76,6 +79,8 @@ UDPSourceSession::UDPSourceSession()
UDPSourceSession::~UDPSourceSession() { UDPSourceSession::~UDPSourceSession() {
(void) Stop(); (void) Stop();
delete[] timeScratch_; delete[] timeScratch_;
delete[] valScratch_;
delete[] tsBatchScratch_;
} }
void UDPSourceSession::ResetCalibration() { void UDPSourceSession::ResetCalibration() {
@@ -84,6 +89,7 @@ void UDPSourceSession::ResetCalibration() {
timeSigCalib_[i] = 0.0; timeSigCalib_[i] = 0.0;
lastPktWallValid_[i] = false; lastPktWallValid_[i] = false;
lastPktWallS_[i] = 0.0; lastPktWallS_[i] = 0.0;
accScalarPrevN_[i] = 0u;
} }
} }
@@ -227,7 +233,11 @@ void UDPSourceSession::ParseConfigPayload(const uint8 *payload, uint32 size) {
} }
if (maxElems > timeScratchLen_) { if (maxElems > timeScratchLen_) {
delete[] timeScratch_; delete[] timeScratch_;
delete[] valScratch_;
delete[] tsBatchScratch_;
timeScratch_ = new float64[maxElems]; timeScratch_ = new float64[maxElems];
valScratch_ = new float64[maxElems];
tsBatchScratch_ = new float64[maxElems];
timeScratchLen_ = maxElems; timeScratchLen_ = maxElems;
} }
@@ -369,17 +379,19 @@ void UDPSourceSession::ParseDataPayload(const uint8 *payload, uint32 size) {
} else { } else {
anchorIsFirstSample = false; anchorIsFirstSample = false;
} }
/* Batch decode: compute all timestamps and values, then batch write. */
const float64 dt = (desc.samplingRate > 0.0) ? (1.0 / desc.samplingRate) : 0.0; const float64 dt = (desc.samplingRate > 0.0) ? (1.0 / desc.samplingRate) : 0.0;
DecodeElems(payload, sigOff[s], desc, nElems, valScratch_);
for (uint32 e = 0u; e < nElems; e++) { for (uint32 e = 0u; e < nElems; e++) {
const float64 val = DecodeOneElem(payload, sigOff[s], desc, e); tsBatchScratch_[e] = anchorIsFirstSample
const float64 t = anchorIsFirstSample
? (anchor + static_cast<float64>(e) * dt) ? (anchor + static_cast<float64>(e) * dt)
: (anchor - static_cast<float64>(nElems - 1u - e) * dt); : (anchor - static_cast<float64>(nElems - 1u - e) * dt);
WriteSample(s, e, t, val);
} }
WriteSampleBatch(s, tsBatchScratch_, valScratch_, nElems);
} }
else if (isFullArray) { else if (isFullArray) {
/* Per-element timestamps from the referenced time-signal array. */ /* Per-element timestamps from the referenced time-signal array.
* Batch decode + batch write (single ring lock per signal). */
if (hasTimeSig && (sigElems[tIdx] >= nElems) && if (hasTimeSig && (sigElems[tIdx] >= nElems) &&
(nElems <= timeScratchLen_)) { (nElems <= timeScratchLen_)) {
DecodeElems(payload, sigOff[tIdx], descs[tIdx], nElems, timeScratch_); DecodeElems(payload, sigOff[tIdx], descs[tIdx], nElems, timeScratch_);
@@ -390,15 +402,18 @@ void UDPSourceSession::ParseDataPayload(const uint8 *payload, uint32 size) {
timeSigCalibValid_[tIdx] = true; timeSigCalibValid_[tIdx] = true;
} }
const float64 calib = timeSigCalib_[tIdx]; const float64 calib = timeSigCalib_[tIdx];
DecodeElems(payload, sigOff[s], desc, nElems, valScratch_);
for (uint32 e = 0u; e < nElems; e++) { for (uint32 e = 0u; e < nElems; e++) {
const float64 val = DecodeOneElem(payload, sigOff[s], desc, e); tsBatchScratch_[e] = calib + timeScratch_[e] * timerToSec;
WriteSample(s, e, calib + timeScratch_[e] * timerToSec, val);
} }
WriteSampleBatch(s, tsBatchScratch_, valScratch_, nElems);
} else { } else {
/* No time signal: all samples get arrival wall time. */
DecodeElems(payload, sigOff[s], desc, nElems, valScratch_);
for (uint32 e = 0u; e < nElems; e++) { for (uint32 e = 0u; e < nElems; e++) {
const float64 val = DecodeOneElem(payload, sigOff[s], desc, e); tsBatchScratch_[e] = wallNowS;
WriteSample(s, e, wallNowS, val);
} }
WriteSampleBatch(s, tsBatchScratch_, valScratch_, nElems);
} }
} }
else if (numElements == 1u) { else if (numElements == 1u) {
@@ -407,22 +422,55 @@ void UDPSourceSession::ParseDataPayload(const uint8 *payload, uint32 size) {
const float64 val = DecodeOneElem(payload, sigOff[s], desc, 0u); const float64 val = DecodeOneElem(payload, sigOff[s], desc, 0u);
WriteSample(s, 0u, wallNowS, val); WriteSample(s, 0u, wallNowS, val);
} }
else if (lastPktWallValid_[s] && (wallNowS > lastPktWallS_[s])) { else {
/* Accumulated scalar: the packet carries one sample per RT /* Accumulated scalar: reconstruct per-sample timestamps from
* cycle — spread them across the inter-packet wall-clock gap * the packet's embedded sender HRT (the acquisition time of the
* (same scheme as packed arrays) instead of stamping them all * oldest sample), NOT the packet arrival time. The kernel
* with the arrival time, which renders as a stair plot. */ * frequently delivers several queued datagrams in one burst, so
const float64 dt = (wallNowS - lastPktWallS_[s]) / * two packets can be processed microseconds apart even though
static_cast<float64>(nElems); * each represents ~10 ms of signal; arrival-time interpolation
const float64 base = lastPktWallS_[s]; * then crams a packet's samples into that tiny gap, rendering
* the trace as a sawtooth ("chainsaw"). The sender HRT is
* immune to this because it is sampled at acquisition.
*
* hrtTimestamp is the HRT counter of sample 0; hrtFreq_ (the
* local HRT frequency, identical to the sender on the same
* host) converts it to seconds, then a one-time calibration
* maps the sender clock onto wall-clock. */
const float64 hrt0Sec = static_cast<float64>(hrtTimestamp) /
hrtFreq_;
if ((!timeSigCalibValid_[s]) ||
(Fabs((timeSigCalib_[s] + hrt0Sec) - wallNowS) >
kRecalibThresholdS)) {
timeSigCalib_[s] = wallNowS - hrt0Sec;
timeSigCalibValid_[s] = true;
}
/* Per-sample dt: samplingRate if present, else derive it from
* the sender-HRT gap to the previous packet divided by that
* packet's sample count (the flushes carry contiguous RT
* cycles, so this is exactly one cycle period). */
float64 dt;
if (desc.samplingRate > 0.0) {
dt = 1.0 / desc.samplingRate;
} else if (lastPktWallValid_[s] && (accScalarPrevN_[s] > 0u) &&
(hrt0Sec > lastPktWallS_[s])) {
dt = (hrt0Sec - lastPktWallS_[s]) /
static_cast<float64>(accScalarPrevN_[s]);
} else {
dt = 1.0e-3; /* 1 kHz default until the gap is known */
}
const float64 base = timeSigCalib_[s] + hrt0Sec;
DecodeElems(payload, sigOff[s], desc, nElems, valScratch_);
for (uint32 e = 0u; e < nElems; e++) { for (uint32 e = 0u; e < nElems; e++) {
const float64 val = DecodeOneElem(payload, sigOff[s], desc, e); tsBatchScratch_[e] = base + static_cast<float64>(e) * dt;
WriteSample(s, 0u, base + static_cast<float64>(e + 1u) * dt, val);
} }
} WriteSampleBatch(s, tsBatchScratch_, valScratch_, nElems);
if (nElems > 1u) {
lastPktWallS_[s] = wallNowS; lastPktWallS_[s] = hrt0Sec; /* sender seconds for next dt */
lastPktWallValid_[s] = true; lastPktWallValid_[s] = true;
accScalarPrevN_[s] = nElems;
} }
} }
else { else {
@@ -435,14 +483,23 @@ void UDPSourceSession::ParseDataPayload(const uint8 *payload, uint32 size) {
* and overlaps the next packet under jitter), elements span * and overlaps the next packet under jitter), elements span
* (lastPktWall, wallNow]: the samples were acquired before the * (lastPktWall, wallNow]: the samples were acquired before the
* packet arrived, and this keeps ring time strictly monotonic. */ * packet arrived, and this keeps ring time strictly monotonic. */
if (lastPktWallValid_[s] && (wallNowS > lastPktWallS_[s])) { if (lastPktWallValid_[s] && (wallNowS >= lastPktWallS_[s])) {
const float64 dt = (wallNowS - lastPktWallS_[s]) / float64 dt;
float64 base;
if (wallNowS > lastPktWallS_[s]) {
dt = (wallNowS - lastPktWallS_[s]) /
static_cast<float64>(nElems); static_cast<float64>(nElems);
const float64 base = lastPktWallS_[s]; base = lastPktWallS_[s];
for (uint32 e = 0u; e < nElems; e++) { } else {
const float64 val = DecodeOneElem(payload, sigOff[s], desc, e); dt = (desc.samplingRate > 0.0)
WriteSample(s, e, base + static_cast<float64>(e + 1u) * dt, val); ? (1.0 / desc.samplingRate) : 1.0e-6;
base = lastPktWallS_[s] - static_cast<float64>(nElems) * dt;
} }
DecodeElems(payload, sigOff[s], desc, nElems, valScratch_);
for (uint32 e = 0u; e < nElems; e++) {
tsBatchScratch_[e] = base + static_cast<float64>(e + 1u) * dt;
}
WriteSampleBatch(s, tsBatchScratch_, valScratch_, nElems);
} }
lastPktWallS_[s] = wallNowS; lastPktWallS_[s] = wallNowS;
lastPktWallValid_[s] = true; lastPktWallValid_[s] = true;
@@ -187,6 +187,22 @@ private:
} }
} }
/** @brief Batch ring write for signal @p s, with optional trigger check.
* Uses WriteBatch internally (single lock acquisition for all samples). */
inline void WriteSampleBatch(uint32 s, const float64 *t, const float64 *v,
uint32 n) {
rings_[s].WriteBatch(t, v, n);
/* Trigger check must still run per-sample for the watched signal. */
if ((trigSigIdx_ >= 0) && (static_cast<uint32>(trigSigIdx_) == s) &&
(trigEngine_ != static_cast<TriggerEngine *>(0))) {
for (uint32 e = 0u; e < n; e++) {
if ((trigElemIdx_ < 0) || (static_cast<uint32>(trigElemIdx_) == e)) {
trigEngine_->CheckSample(t[e], v[e]);
}
}
}
}
/** /**
* @brief Decode @p nElems consecutive elements of signal @p desc starting * @brief Decode @p nElems consecutive elements of signal @p desc starting
* at payload offset @p off into @p out (dequantised physical values). * at payload offset @p off into @p out (dequantised physical values).
@@ -253,12 +269,23 @@ private:
bool timeSigCalibValid_[UDPSS_MAX_SIGNALS]; bool timeSigCalibValid_[UDPSS_MAX_SIGNALS];
/* Last packet arrival wall time per signal — used to interpolate per-element /* Last packet arrival wall time per signal — used to interpolate per-element
* timestamps for packed TIMEMODE_PACKET arrays (Go hub lastPktNs). */ * timestamps for packed TIMEMODE_PACKET arrays (Go hub lastPktNs).
* For accumulated scalars this instead holds the previous packet's
* sender-clock seconds (HRT-derived), used to derive the per-sample dt. */
float64 lastPktWallS_[UDPSS_MAX_SIGNALS]; float64 lastPktWallS_[UDPSS_MAX_SIGNALS];
bool lastPktWallValid_[UDPSS_MAX_SIGNALS]; bool lastPktWallValid_[UDPSS_MAX_SIGNALS];
/* Scratch for decoding referenced time-signal arrays (receive thread only). */ /* Accumulated-scalar timing: HRT counter frequency (local == sender on the
float64 *timeScratch_; * same host) and the previous packet's sample count, used to reconstruct
* per-sample timestamps from the embedded sender HRT instead of the (UDP
* burst-sensitive) packet arrival time. */
float64 hrtFreq_;
uint32 accScalarPrevN_[UDPSS_MAX_SIGNALS];
/* Scratch buffers for decoding arrays (receive thread only). */
float64 *timeScratch_; ///< Time values scratch
float64 *valScratch_; ///< Data values scratch
float64 *tsBatchScratch_; ///< Timestamps scratch for batch write
uint32 timeScratchLen_; uint32 timeScratchLen_;
/* Trigger hookup (resolution cache is receive-thread only). */ /* Trigger hookup (resolution cache is receive-thread only). */
@@ -0,0 +1,25 @@
#############################################################
#
# 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
@@ -0,0 +1,25 @@
#############################################################
#
# 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
@@ -0,0 +1,60 @@
#############################################################
#
# 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 = UDPStreamerClient.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$(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
all: $(OBJS) \
$(BUILD_DIR)/UDPStreamerClient$(LIBEXT) \
$(BUILD_DIR)/UDPStreamerClient$(DLLEXT)
echo $(OBJS)
include depends.$(TARGET)
include $(MAKEDEFAULTDIR)/MakeStdLibRules.$(TARGET)
@@ -0,0 +1,564 @@
/**
* @file UDPStreamerClient.cpp
* @brief Source file for class UDPStreamerClient
* @date 24/06/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 UDPStreamerClient (public, protected, and private).
*/
#define DLL_API
/*---------------------------------------------------------------------------*/
/* Standard header includes */
/*---------------------------------------------------------------------------*/
#include <string.h>
/*---------------------------------------------------------------------------*/
/* Project header includes */
/*---------------------------------------------------------------------------*/
#include "AdvancedErrorManagement.h"
#include "ConfigurationDatabase.h"
#include "GlobalObjectsDatabase.h"
#include "MemoryOperationsHelper.h"
#include "UDPSProtocol.h"
#include "UDPStreamerClient.h"
/*---------------------------------------------------------------------------*/
/* Static definitions */
/*---------------------------------------------------------------------------*/
namespace MARTe {
/** Default server address when none is specified. */
static const char8 *const UDPS_CLIENT_DEFAULT_ADDR = "127.0.0.1";
/** Default port used when none is specified. */
static const uint16 UDPS_CLIENT_DEFAULT_PORT = 44500u;
/** Default data-port offset: dataPort = port + this when DataPort is absent. */
static const uint16 UDPS_CLIENT_DEFAULT_DP_OFFSET = 1u;
/** Default max payload per UDP datagram (bytes). */
static const uint32 UDPS_CLIENT_DEFAULT_MAX_PAYLOAD = 1400u;
/** Bytes prepended to each DATA payload for the HRT packet timestamp. */
static const uint32 UDPS_CLIENT_TIMESTAMP_BYTES = 8u;
/*---------------------------------------------------------------------------*/
/* Decode helpers */
/*---------------------------------------------------------------------------*/
/**
* @brief Reads one quantised wire element (1 or 2 bytes) as a float64,
* mirroring UDPSourceSession::DecodeRawValue for the quantised type codes.
*/
static float64 DecodeQuantRaw(const uint8 *ptr, uint8 quantType) {
float64 raw = 0.0;
switch (quantType) {
case UDPS_QUANT_UINT8: {
uint8 v = 0u; (void) memcpy(&v, ptr, 1u); raw = static_cast<float64>(v);
break;
}
case UDPS_QUANT_INT8: {
int8 v = 0; (void) memcpy(&v, ptr, 1u); raw = static_cast<float64>(v);
break;
}
case UDPS_QUANT_UINT16: {
uint16 v = 0u; (void) memcpy(&v, ptr, 2u); raw = static_cast<float64>(v);
break;
}
case UDPS_QUANT_INT16: {
int16 v = 0; (void) memcpy(&v, ptr, 2u); raw = static_cast<float64>(v);
break;
}
default:
break;
}
return raw;
}
/**
* @brief Dequantises a raw value, mirroring UDPSourceSession::DequantizeValue.
*/
static float64 DequantValue(float64 raw, uint8 quantType,
float64 rangeMin, float64 rangeMax) {
const float64 range = rangeMax - rangeMin;
float64 val;
switch (quantType) {
case UDPS_QUANT_UINT8:
val = rangeMin + (raw / 255.0) * range;
break;
case UDPS_QUANT_INT8:
val = rangeMin + ((raw + 127.0) / 254.0) * range;
break;
case UDPS_QUANT_UINT16:
val = rangeMin + (raw / 65535.0) * range;
break;
case UDPS_QUANT_INT16:
val = rangeMin + ((raw + 32767.0) / 65534.0) * range;
break;
default:
val = raw;
break;
}
return val;
}
/*---------------------------------------------------------------------------*/
/* Method definitions */
/*---------------------------------------------------------------------------*/
UDPStreamerClient::UDPStreamerClient() :
MemoryDataSourceI(),
UDPSClientListener(),
client() {
serverAddress = UDPS_CLIENT_DEFAULT_ADDR;
port = UDPS_CLIENT_DEFAULT_PORT;
maxPayloadSize = UDPS_CLIENT_DEFAULT_MAX_PAYLOAD;
cpuMask = 0xFFFFFFFFu;
stackSize = THREADS_DEFAULT_STACKSIZE;
dataPort = UDPS_CLIENT_DEFAULT_PORT + UDPS_CLIENT_DEFAULT_DP_OFFSET;
useMulticast = false;
numSigs = 0u;
signalInfos = NULL_PTR(UDPStreamerClientSignal *);
totalSrcBytes = 0u;
configValidated = false;
publishMode = UDPS_PUBLISH_STRICT;
receiverUp = false;
readyBuffer = NULL_PTR(uint8 *);
scratchBuffer = NULL_PTR(uint8 *);
newDataReady = false;
if (!dataSem.Create()) {
REPORT_ERROR(ErrorManagement::FatalError, "Could not create EventSem.");
}
bufMutex.Create(false);
}
UDPStreamerClient::~UDPStreamerClient() {
(void) dataSem.Post();
if (receiverUp) {
(void) client.Stop();
receiverUp = false;
}
HeapI *heap = GlobalObjectsDatabase::Instance()->GetStandardHeap();
if (signalInfos != NULL_PTR(UDPStreamerClientSignal *)) {
delete[] signalInfos;
signalInfos = NULL_PTR(UDPStreamerClientSignal *);
}
if (readyBuffer != NULL_PTR(uint8 *)) {
heap->Free(reinterpret_cast<void *&>(readyBuffer));
}
if (scratchBuffer != NULL_PTR(uint8 *)) {
heap->Free(reinterpret_cast<void *&>(scratchBuffer));
}
(void) dataSem.Close();
}
bool UDPStreamerClient::Initialise(StructuredDataI &data) {
bool ok = MemoryDataSourceI::Initialise(data);
if (ok) {
StreamString addr = "";
if (!data.Read("ServerAddress", addr) || (addr.Size() == 0u)) {
addr = UDPS_CLIENT_DEFAULT_ADDR;
REPORT_ERROR(ErrorManagement::Information,
"ServerAddress not specified; using default %s.",
UDPS_CLIENT_DEFAULT_ADDR);
}
serverAddress = addr;
}
if (ok) {
if (!data.Read("Port", port)) {
port = UDPS_CLIENT_DEFAULT_PORT;
REPORT_ERROR(ErrorManagement::Information,
"Port not specified; using default %u.",
static_cast<uint32>(port));
}
}
if (ok) {
if (!data.Read("MaxPayloadSize", maxPayloadSize)) {
maxPayloadSize = UDPS_CLIENT_DEFAULT_MAX_PAYLOAD;
}
}
if (ok) {
if (!data.Read("CPUMask", cpuMask)) {
cpuMask = 0xFFFFFFFFu;
}
}
if (ok) {
if (!data.Read("StackSize", stackSize)) {
stackSize = THREADS_DEFAULT_STACKSIZE;
}
}
if (ok) {
StreamString mcastStr = "";
(void) data.Read("MulticastGroup", mcastStr);
if (mcastStr.Size() > 0u) {
multicastGroup = mcastStr;
useMulticast = true;
uint16 dp = 0u;
if (!data.Read("DataPort", dp)) {
dp = port + UDPS_CLIENT_DEFAULT_DP_OFFSET;
}
dataPort = dp;
REPORT_ERROR(ErrorManagement::Information,
"Multicast mode: group=%s, server=%s, controlPort=%u, dataPort=%u.",
multicastGroup.Buffer(), serverAddress.Buffer(),
static_cast<uint32>(port), static_cast<uint32>(dataPort));
}
else {
useMulticast = false;
}
}
/* Forward the transport parameters to the shared UDPSClient receiver,
* translating the DataSource parameter names to the UDPSClient keys. */
if (ok) {
ConfigurationDatabase cdb;
ok = cdb.Write("ServerAddr", serverAddress);
if (ok) { ok = cdb.Write("Port", static_cast<uint32>(port)); }
if (ok && useMulticast) {
ok = cdb.Write("MulticastGroup", multicastGroup);
if (ok) { ok = cdb.Write("DataPort", static_cast<uint32>(dataPort)); }
}
if (ok) { ok = cdb.Write("MaxPayloadSize", maxPayloadSize); }
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;
}
bool UDPStreamerClient::SetConfiguredDatabase(StructuredDataI &data) {
bool ok = MemoryDataSourceI::SetConfiguredDatabase(data);
if (!ok) {
return false;
}
numSigs = GetNumberOfSignals();
if (numSigs == 0u) {
REPORT_ERROR(ErrorManagement::ParametersError,
"At least one signal must be defined.");
return false;
}
signalInfos = new UDPStreamerClientSignal[numSigs];
totalSrcBytes = 0u;
for (uint32 i = 0u; (i < numSigs) && ok; i++) {
StreamString sigName;
ok = GetSignalName(i, sigName);
if (!ok) {
REPORT_ERROR(ErrorManagement::FatalError,
"Could not get name for signal %u.", i);
break;
}
uint32 nelems = 1u;
(void) GetSignalNumberOfElements(i, nelems);
if (nelems == 0u) { nelems = 1u; }
uint32 bsz = 0u;
(void) GetSignalByteSize(i, bsz);
signalInfos[i].name = sigName;
signalInfos[i].type = GetSignalType(i);
signalInfos[i].quantType = UDPS_QUANT_NONE;
signalInfos[i].numElements = nelems;
signalInfos[i].rangeMin = 0.0;
signalInfos[i].rangeMax = 1.0;
signalInfos[i].srcByteSize = bsz;
signalInfos[i].wireElemBytes = (nelems > 0u) ? (bsz / nelems) : bsz;
signalInfos[i].bufferOffset = totalSrcBytes;
totalSrcBytes += bsz;
}
return ok;
}
bool UDPStreamerClient::AllocateMemory() {
bool ok = MemoryDataSourceI::AllocateMemory();
if (!ok) {
return false;
}
if (totalSrcBytes == 0u) {
totalSrcBytes = stateMemorySize;
}
HeapI *heap = GlobalObjectsDatabase::Instance()->GetStandardHeap();
readyBuffer = reinterpret_cast<uint8 *>(heap->Malloc(totalSrcBytes));
if (readyBuffer == NULL_PTR(uint8 *)) {
REPORT_ERROR(ErrorManagement::FatalError, "Could not allocate readyBuffer.");
return false;
}
(void) MemoryOperationsHelper::Set(readyBuffer, 0, totalSrcBytes);
scratchBuffer = reinterpret_cast<uint8 *>(heap->Malloc(totalSrcBytes));
if (scratchBuffer == NULL_PTR(uint8 *)) {
REPORT_ERROR(ErrorManagement::FatalError, "Could not allocate scratchBuffer.");
return false;
}
(void) MemoryOperationsHelper::Set(scratchBuffer, 0, totalSrcBytes);
return true;
}
const char8 *UDPStreamerClient::GetBrokerName(StructuredDataI &data,
const SignalDirection direction) {
const char8 *brokerName = "";
if (direction == InputSignals) {
brokerName = "MemoryMapSynchronisedInputBroker";
}
return brokerName;
}
bool UDPStreamerClient::PrepareNextState(const char8 *const currentStateName,
const char8 *const nextStateName) {
bool ok = true;
if (!receiverUp) {
ok = client.Start();
if (ok) {
receiverUp = true;
}
else {
REPORT_ERROR(ErrorManagement::FatalError,
"Could not start the UDPSClient receiver.");
}
}
return ok;
}
bool UDPStreamerClient::Synchronise() {
ErrorManagement::ErrorType waitErr = dataSem.ResetWait(TimeoutType(10u));
if (waitErr == ErrorManagement::NoError) {
bufMutex.FastLock(TTInfiniteWait);
(void) MemoryOperationsHelper::Copy(memory, readyBuffer, totalSrcBytes);
newDataReady = false;
bufMutex.FastUnLock();
}
return true;
}
/*---------------------------------------------------------------------------*/
/* UDPSClientListener callbacks */
/*---------------------------------------------------------------------------*/
void UDPStreamerClient::OnUDPSConfig(const uint8 *payload, uint32 payloadSize) {
if (payloadSize < 4u) {
REPORT_ERROR(ErrorManagement::ParametersError,
"CONFIG payload too small (%u bytes).", payloadSize);
return;
}
uint32 serverNumSigs = 0u;
(void) memcpy(&serverNumSigs, payload, 4u);
if (serverNumSigs != numSigs) {
REPORT_ERROR(ErrorManagement::ParametersError,
"CONFIG signal count mismatch: server=%u, client=%u.",
serverNumSigs, numSigs);
return;
}
const uint32 needed = 4u + (numSigs * UDPS_SIGNAL_DESC_SIZE);
if (payloadSize < needed) {
REPORT_ERROR(ErrorManagement::ParametersError,
"CONFIG payload truncated (%u < %u bytes).", payloadSize, needed);
return;
}
bool ok = true;
for (uint32 i = 0u; (i < numSigs) && ok; i++) {
UDPSSignalDescriptor desc;
(void) memcpy(&desc, payload + 4u + (i * UDPS_SIGNAL_DESC_SIZE),
UDPS_SIGNAL_DESC_SIZE);
char8 nameBuf[UDPS_MAX_SIGNAL_NAME + 1u];
(void) memcpy(nameBuf, desc.name, UDPS_MAX_SIGNAL_NAME);
nameBuf[UDPS_MAX_SIGNAL_NAME] = '\0';
StreamString serverName = nameBuf;
if (signalInfos[i].name != serverName) {
REPORT_ERROR(ErrorManagement::ParametersError,
"CONFIG signal %u name mismatch: server=%s, client=%s.",
i, serverName.Buffer(), signalInfos[i].name.Buffer());
ok = false;
break;
}
uint32 serverElems = desc.numRows * desc.numCols;
if (serverElems == 0u) { serverElems = 1u; }
if (serverElems != signalInfos[i].numElements) {
REPORT_ERROR(ErrorManagement::ParametersError,
"CONFIG signal %u element count mismatch: server=%u, client=%u.",
i, serverElems, signalInfos[i].numElements);
ok = false;
break;
}
signalInfos[i].quantType = desc.quantType;
signalInfos[i].rangeMin = desc.rangeMin;
signalInfos[i].rangeMax = desc.rangeMax;
if (desc.quantType != UDPS_QUANT_NONE) {
uint32 q = 0u;
if ((desc.quantType == UDPS_QUANT_UINT8) ||
(desc.quantType == UDPS_QUANT_INT8)) {
q = 1u;
}
else if ((desc.quantType == UDPS_QUANT_UINT16) ||
(desc.quantType == UDPS_QUANT_INT16)) {
q = 2u;
}
signalInfos[i].wireElemBytes = q;
}
}
if (ok) {
const uint32 pmOffset = needed;
publishMode = (payloadSize > pmOffset) ? payload[pmOffset] : UDPS_PUBLISH_STRICT;
bufMutex.FastLock(TTInfiniteWait);
configValidated = true;
bufMutex.FastUnLock();
REPORT_ERROR(ErrorManagement::Information,
"CONFIG validated: %u signals, publishMode=%u, totalSrcBytes=%u.",
numSigs, static_cast<uint32>(publishMode), totalSrcBytes);
}
}
void UDPStreamerClient::OnUDPSData(const uint8 *payload, uint32 payloadSize) {
if (!configValidated) {
return;
}
if (payloadSize < UDPS_CLIENT_TIMESTAMP_BYTES) {
return;
}
(void) MemoryOperationsHelper::Set(scratchBuffer, 0, totalSrcBytes);
DecodeSnapshot(payload, payloadSize, scratchBuffer);
bufMutex.FastLock(TTInfiniteWait);
(void) MemoryOperationsHelper::Copy(readyBuffer, scratchBuffer, totalSrcBytes);
newDataReady = true;
bufMutex.FastUnLock();
(void) dataSem.Post();
}
void UDPStreamerClient::OnUDPSConnected() {
REPORT_ERROR(ErrorManagement::Information,
"UDPStreamerClient connected to %s:%u.",
serverAddress.Buffer(), static_cast<uint32>(port));
}
void UDPStreamerClient::OnUDPSDisconnected() {
bufMutex.FastLock(TTInfiniteWait);
configValidated = false;
bufMutex.FastUnLock();
REPORT_ERROR(ErrorManagement::Information, "UDPStreamerClient disconnected.");
}
/*---------------------------------------------------------------------------*/
/* Decode logic */
/*---------------------------------------------------------------------------*/
void UDPStreamerClient::DecodeSnapshot(const uint8 *payload, uint32 size,
uint8 *dst) {
uint32 offset = UDPS_CLIENT_TIMESTAMP_BYTES;
uint32 numSamples = 1u;
if (publishMode == UDPS_PUBLISH_ACCUMULATE) {
if ((offset + 4u) > size) { return; }
(void) memcpy(&numSamples, payload + offset, 4u);
offset += 4u;
if (numSamples == 0u) { numSamples = 1u; }
}
uint32 off = offset;
for (uint32 s = 0u; s < numSigs; s++) {
const UDPStreamerClientSignal &info = signalInfos[s];
const uint32 wireElemBytes = info.wireElemBytes;
if (wireElemBytes == 0u) { return; }
const uint32 ne = info.numElements;
const bool accScalar = (publishMode == UDPS_PUBLISH_ACCUMULATE) && (ne == 1u);
const uint32 elemsToRead = accScalar ? numSamples : ne;
if ((off + (elemsToRead * wireElemBytes)) > size) { return; }
uint8 *d = dst + info.bufferOffset;
if (info.quantType == UDPS_QUANT_NONE) {
if (accScalar) {
/* Publish the most recent accumulated sample. */
const uint8 *srcLast = payload + off +
((numSamples - 1u) * wireElemBytes);
(void) memcpy(d, srcLast, wireElemBytes);
}
else {
(void) memcpy(d, payload + off, ne * wireElemBytes);
}
}
else {
const bool isF32 = (info.type == Float32Bit);
const uint32 startElem = accScalar ? (numSamples - 1u) : 0u;
const uint32 count = accScalar ? 1u : ne;
for (uint32 e = 0u; e < count; e++) {
const uint8 *ptr = payload + off +
((startElem + e) * wireElemBytes);
const float64 raw = DecodeQuantRaw(ptr, info.quantType);
const float64 val = DequantValue(raw, info.quantType,
info.rangeMin, info.rangeMax);
if (isF32) {
float32 f32 = static_cast<float32>(val);
(void) memcpy(d, &f32, 4u);
d += 4u;
}
else {
float64 f64 = val;
(void) memcpy(d, &f64, 8u);
d += 8u;
}
}
}
off += elemsToRead * wireElemBytes;
}
}
CLASS_REGISTER(UDPStreamerClient, "1.0")
} /* namespace MARTe */
@@ -0,0 +1,204 @@
/**
* @file UDPStreamerClient.h
* @brief Header file for class UDPStreamerClient
* @date 24/06/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 UDPStreamerClient
* with all of its public, protected and private members.
*
* The UDPStreamerClient is an input DataSource that receives signal data from
* a UDPStreamer server over the network. It reuses the StreamHub hub's network
* code base: transport, fragment reassembly, multicast and auto-reconnect are
* delegated to the shared UDPSClient receiver, and this class only implements
* the UDPSClientListener callbacks to validate the CONFIG and decode DATA into
* the real-time signal memory.
*/
#ifndef UDPSTREAMERCLIENT_H_
#define UDPSTREAMERCLIENT_H_
/*---------------------------------------------------------------------------*/
/* Standard header includes */
/*---------------------------------------------------------------------------*/
/*---------------------------------------------------------------------------*/
/* Project header includes */
/*---------------------------------------------------------------------------*/
#include "EventSem.h"
#include "FastPollingMutexSem.h"
#include "MemoryDataSourceI.h"
#include "StreamString.h"
#include "TypeDescriptor.h"
#include "UDPSClient.h"
/*---------------------------------------------------------------------------*/
/* Class declaration */
/*---------------------------------------------------------------------------*/
namespace MARTe {
/**
* @brief Per-signal decode metadata resolved from the server CONFIG packet.
*/
struct UDPStreamerClientSignal {
StreamString name; /**< Signal name (must match server). */
TypeDescriptor type; /**< MARTe2 destination type. */
uint8 quantType; /**< UDPS_QUANT_* code from CONFIG. */
uint32 numElements; /**< Element count (scalar = 1). */
float64 rangeMin; /**< Dequantisation lower bound. */
float64 rangeMax; /**< Dequantisation upper bound. */
uint32 srcByteSize; /**< Destination byte size for the signal. */
uint32 wireElemBytes; /**< Bytes per element on the wire. */
uint32 bufferOffset; /**< Offset of this signal in the flat buffer. */
};
/**
* @brief A DataSource that receives signal data from a UDPStreamer server.
*
* @details This input DataSource owns a shared ::MARTe::UDPSClient (the same
* receiver used by the StreamHub hub) which runs a background thread, connects
* to the server, reassembles fragmented updates and auto-reconnects on silence.
* The CONFIG and DATA payloads are delivered through the UDPSClientListener
* callbacks; DATA is decoded into a double-buffered staging area which the
* real-time thread copies into signal memory during Synchronise().
*
* Two operating modes are selected by the presence or absence of MulticastGroup.
*
* @par Top-level configuration parameters
* | Parameter | Type | Default | Description |
* |-----------------|---------|--------------|-------------|
* | ServerAddress | string | "127.0.0.1" | UDPStreamer server IP address. |
* | Port | uint16 | 44500 | Server port (CONNECT/UDP unicast, TCP control multicast). |
* | MulticastGroup | string | *(absent)* | Enables multicast mode. IPv4 multicast address. |
* | DataPort | uint16 | Port+1 | UDP port for DATA datagrams in multicast mode. |
* | MaxPayloadSize | uint32 | 1400 | Maximum payload bytes per datagram (excluding header). |
* | CPUMask | uint32 | 0xFFFFFFFF | CPU affinity bitmask for the background thread. |
* | StackSize | uint32 | 65536 | Stack size in bytes for the background thread. |
*
* The client's signal configuration must match the server's: signal count,
* names and element counts are validated against the received CONFIG.
*/
class UDPStreamerClient : public MemoryDataSourceI,
public UDPSClientListener {
public:
CLASS_REGISTER_DECLARATION()
/**
* @brief Constructor. Initialises all members to safe defaults.
*/
UDPStreamerClient();
/**
* @brief Destructor. Stops the background receiver and frees memory.
*/
virtual ~UDPStreamerClient();
/**
* @brief Parses top-level configuration and configures the UDPSClient.
*/
virtual bool Initialise(StructuredDataI &data);
/**
* @brief Validates signal types and builds the per-signal metadata array.
*/
virtual bool SetConfiguredDatabase(StructuredDataI &data);
/**
* @brief Allocates signal memory plus readyBuffer and scratchBuffer.
*/
virtual bool AllocateMemory();
/**
* @brief Returns "MemoryMapSynchronisedInputBroker" for InputSignals.
*/
virtual const char8 *GetBrokerName(StructuredDataI &data,
const SignalDirection direction);
/**
* @brief Copies the latest received data into signal memory.
*/
virtual bool Synchronise();
/**
* @brief Starts the background receiver on the first state transition.
*/
virtual bool PrepareNextState(const char8 *const currentStateName,
const char8 *const nextStateName);
/* ---- UDPSClientListener callbacks (invoked from the receiver thread) ---- */
/**
* @brief Validates a reassembled CONFIG payload against the local signals.
*/
virtual void OnUDPSConfig(const uint8 *payload, uint32 payloadSize);
/**
* @brief Decodes a reassembled DATA payload into the staging buffer.
*/
virtual void OnUDPSData(const uint8 *payload, uint32 payloadSize);
/**
* @brief Logs that the server connection is established.
*/
virtual void OnUDPSConnected();
/**
* @brief Invalidates the CONFIG so stale data is not published.
*/
virtual void OnUDPSDisconnected();
private:
/**
* @brief Decodes a single snapshot from a DATA payload into @p dst.
* @details Mirrors the StreamHub hub layout (UDPSourceSession): an 8-byte
* HRT timestamp, an optional 4-byte sample count (Accumulate mode), then
* per-signal data in CONFIG order. For accumulated scalars the most recent
* sample is published.
*/
void DecodeSnapshot(const uint8 *payload, uint32 size, uint8 *dst);
/* Configuration parameters */
StreamString serverAddress; /**< Server IP address. */
uint16 port; /**< Server port. */
uint32 maxPayloadSize; /**< Max payload bytes per datagram. */
uint32 cpuMask; /**< Background thread CPU affinity. */
uint32 stackSize; /**< Background thread stack size. */
StreamString multicastGroup; /**< Multicast group IP; empty = unicast. */
uint16 dataPort; /**< UDP port for DATA datagrams (multicast). */
bool useMulticast; /**< True when MulticastGroup is set. */
/* Signal metadata */
uint32 numSigs; /**< Number of signals. */
UDPStreamerClientSignal *signalInfos; /**< Per-signal metadata (heap). */
uint32 totalSrcBytes; /**< Total destination signal bytes. */
bool configValidated; /**< True after a valid CONFIG. */
uint8 publishMode; /**< UDPS_PUBLISH_* from CONFIG. */
/* Shared receiver (same code base as the StreamHub hub) */
UDPSClient client; /**< Transport, reassembly and reconnect. */
bool receiverUp; /**< True once the receiver thread is started. */
/* Double-buffering for RT safety */
uint8 *readyBuffer; /**< Protected copy of decoded signal data. */
uint8 *scratchBuffer; /**< Receiver-thread-private decode buffer. */
FastPollingMutexSem bufMutex; /**< Protects readyBuffer. */
EventSem dataSem; /**< Wakes the RT thread on new data. */
bool newDataReady; /**< Set when readyBuffer has fresh data. */
};
} /* namespace MARTe */
#endif /* UDPSTREAMERCLIENT_H_ */
@@ -0,0 +1,140 @@
../../../..//Build/x86-linux/Components/DataSources/UDPStreamerClient/UDPStreamerClient.o: \
UDPStreamerClient.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 \
../../../..//Common/UDP/UDPSProtocol.h 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 \
/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/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/L1Portability/HighResolutionTimer.h \
/home/martino/workspace/MARTe2/Source/Core/FileSystem/L1Portability/InternetHost.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
@@ -0,0 +1,143 @@
UDPStreamerClient.o: UDPStreamerClient.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/Scheduler/L3Services/EmbeddedThreadI.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/EmbeddedServiceMethodBinderT.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/L1Portability/HighResolutionTimer.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L5GAMs/MemoryMapSynchronisedInputBroker.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L5GAMs/MemoryMapInputBroker.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L5GAMs/MemoryMapBroker.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L5GAMs/BrokerI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L5GAMs/DataSourceI.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/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 \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L5GAMs/StatefulI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L5GAMs/ExecutableI.h \
/home/martino/workspace/MARTe2/Source/Core/BareMetal/L1Portability/Sleep.h \
UDPStreamerClient.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/Scheduler/L3Services/EmbeddedServiceMethodBinderI.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/Scheduler/L3Services/SingleThreadService.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/EmbeddedServiceI.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/EmbeddedThreadI.h \
/home/martino/workspace/MARTe2/Source/Core/Scheduler/L3Services/EmbeddedThread.h \
../UDPStreamer/UDPStreamer.h
+224 -69
View File
@@ -11,11 +11,10 @@
* Ch1 (1 kHz, 1 V, phase 0) * Ch1 (1 kHz, 1 V, phase 0)
* Ch2 (5 kHz, 0.5 V, phase 90 deg) * Ch2 (5 kHz, 0.5 V, phase 90 deg)
* *
* Thread3 5 kHz — 2 array sines @ 5 Msps (1000 elem × 5 kHz) * Thread3 5 kHz — 2 array sines @ 5 Msps (1000 elem × 5 kHz, Ratio=5 → 1 kHz pub)
* Ch3 (10 kHz, 2 V, phase 0) * Ch3 (10 kHz, 2 V, phase 0)
* Ch4 (50 kHz, 1.5 V, phase 45 deg) * Ch4 (50 kHz, 1.5 V, phase 45 deg)
*/ */
$App = { $App = {
Class = RealTimeApplication Class = RealTimeApplication
@@ -23,7 +22,6 @@ $App = {
Class = ReferenceContainer Class = ReferenceContainer
// ── Thread1: 1 kHz scalar sines (1 ksps) ──────────────────────────── // ── Thread1: 1 kHz scalar sines (1 ksps) ────────────────────────────
+TimerGAM = { +TimerGAM = {
Class = IOGAM Class = IOGAM
InputSignals = { InputSignals = {
@@ -38,8 +36,14 @@ $App = {
} }
} }
OutputSignals = { OutputSignals = {
Counter = { DataSource = DDB1 Type = uint32 } Counter = {
Time = { DataSource = DDB1 Type = uint32 } DataSource = DDB1
Type = uint32
}
Time = {
DataSource = DDB1
Type = uint32
}
} }
} }
@@ -51,7 +55,10 @@ $App = {
Offset = 0.0 Offset = 0.0
SamplingRate = 1000.0 SamplingRate = 1000.0
OutputSignals = { OutputSignals = {
Sine1 = { DataSource = DDB1 Type = float32 } Sine1 = {
DataSource = DDB1
Type = float32
}
} }
} }
@@ -63,28 +70,54 @@ $App = {
Offset = 0.0 Offset = 0.0
SamplingRate = 1000.0 SamplingRate = 1000.0
OutputSignals = { OutputSignals = {
Sine2 = { DataSource = DDB1 Type = float32 } Sine2 = {
DataSource = DDB1
Type = float32
}
} }
} }
+StreamerGAM1 = { +StreamerGAM1 = {
Class = IOGAM Class = IOGAM
InputSignals = { InputSignals = {
Counter = { DataSource = DDB1 Type = uint32 } Counter = {
Time = { DataSource = DDB1 Type = uint32 } DataSource = DDB1
Sine1 = { DataSource = DDB1 Type = float32 } Type = uint32
Sine2 = { DataSource = DDB1 Type = float32 } }
Time = {
DataSource = DDB1
Type = uint32
}
Sine1 = {
DataSource = DDB1
Type = float32
}
Sine2 = {
DataSource = DDB1
Type = float32
}
} }
OutputSignals = { OutputSignals = {
Counter = { DataSource = Streamer1 Type = uint32 } Counter = {
Time = { DataSource = Streamer1 Type = uint32 } DataSource = Streamer1
Sine1 = { DataSource = Streamer1 Type = float32 } Type = uint32
Sine2 = { DataSource = Streamer1 Type = float32 } }
Time = {
DataSource = Streamer1
Type = uint32
}
Sine1 = {
DataSource = Streamer1
Type = float32
}
Sine2 = {
DataSource = Streamer1
Type = float32
}
} }
} }
// ── Thread2: 1 kHz, 1000-element arrays → 1 Msps ─────────────────── // ── Thread2: 1 kHz, 1000-element arrays → 1 Msps ───────────────────
+MedTimerGAM = { +MedTimerGAM = {
Class = IOGAM Class = IOGAM
InputSignals = { InputSignals = {
@@ -95,7 +128,10 @@ $App = {
} }
} }
OutputSignals = { OutputSignals = {
Time = { DataSource = DDB2 Type = uint32 } Time = {
DataSource = DDB2
Type = uint32
}
} }
} }
@@ -136,9 +172,12 @@ $App = {
+TimeArrayGAM1 = { +TimeArrayGAM1 = {
Class = TimeArrayGAM Class = TimeArrayGAM
SamplingRate = 1000000.0 SamplingRate = 1000000.0
Anchor = FirstSample Anchor = "FirstSample"
InputSignals = { InputSignals = {
Time = { DataSource = DDB2 Type = uint32 } Time = {
DataSource = DDB2
Type = uint32
}
} }
OutputSignals = { OutputSignals = {
TimeArray1 = { TimeArray1 = {
@@ -153,19 +192,48 @@ $App = {
+StreamerGAM2 = { +StreamerGAM2 = {
Class = IOGAM Class = IOGAM
InputSignals = { InputSignals = {
TimeArray1 = { DataSource = DDB2 Type = uint64 NumberOfDimensions = 1 NumberOfElements = 1000 } TimeArray1 = {
Ch1 = { DataSource = DDB2 Type = float32 NumberOfDimensions = 1 NumberOfElements = 1000 } DataSource = DDB2
Ch2 = { DataSource = DDB2 Type = float32 NumberOfDimensions = 1 NumberOfElements = 1000 } Type = uint64
NumberOfDimensions = 1
NumberOfElements = 1000
}
Ch1 = {
DataSource = DDB2
Type = float32
NumberOfDimensions = 1
NumberOfElements = 1000
}
Ch2 = {
DataSource = DDB2
Type = float32
NumberOfDimensions = 1
NumberOfElements = 1000
}
} }
OutputSignals = { OutputSignals = {
TimeArray1 = { DataSource = Streamer2 Type = uint64 NumberOfDimensions = 1 NumberOfElements = 1000 } TimeArray1 = {
Ch1 = { DataSource = Streamer2 Type = float32 NumberOfDimensions = 1 NumberOfElements = 1000 } DataSource = Streamer2
Ch2 = { DataSource = Streamer2 Type = float32 NumberOfDimensions = 1 NumberOfElements = 1000 } Type = uint64
NumberOfDimensions = 1
NumberOfElements = 1000
}
Ch1 = {
DataSource = Streamer2
Type = float32
NumberOfDimensions = 1
NumberOfElements = 1000
}
Ch2 = {
DataSource = Streamer2
Type = float32
NumberOfDimensions = 1
NumberOfElements = 1000
}
} }
} }
// ── Thread3: 5 kHz, 1000-element arrays → 5 Msps ─────────────────── // ── Thread3: 5 kHz, 1000-element arrays → 5 Msps ───────────────────
+FastTimerGAM = { +FastTimerGAM = {
Class = IOGAM Class = IOGAM
InputSignals = { InputSignals = {
@@ -176,7 +244,10 @@ $App = {
} }
} }
OutputSignals = { OutputSignals = {
Time = { DataSource = DDB3 Type = uint32 } Time = {
DataSource = DDB3
Type = uint32
}
} }
} }
@@ -217,9 +288,12 @@ $App = {
+TimeArrayGAM2 = { +TimeArrayGAM2 = {
Class = TimeArrayGAM Class = TimeArrayGAM
SamplingRate = 5000000.0 SamplingRate = 5000000.0
Anchor = FirstSample Anchor = "FirstSample"
InputSignals = { InputSignals = {
Time = { DataSource = DDB3 Type = uint32 } Time = {
DataSource = DDB3
Type = uint32
}
} }
OutputSignals = { OutputSignals = {
TimeArray2 = { TimeArray2 = {
@@ -234,14 +308,44 @@ $App = {
+StreamerGAM3 = { +StreamerGAM3 = {
Class = IOGAM Class = IOGAM
InputSignals = { InputSignals = {
TimeArray2 = { DataSource = DDB3 Type = uint64 NumberOfDimensions = 1 NumberOfElements = 1000 } TimeArray2 = {
Ch3 = { DataSource = DDB3 Type = float32 NumberOfDimensions = 1 NumberOfElements = 1000 } DataSource = DDB3
Ch4 = { DataSource = DDB3 Type = float32 NumberOfDimensions = 1 NumberOfElements = 1000 } Type = uint64
NumberOfDimensions = 1
NumberOfElements = 1000
}
Ch3 = {
DataSource = DDB3
Type = float32
NumberOfDimensions = 1
NumberOfElements = 1000
}
Ch4 = {
DataSource = DDB3
Type = float32
NumberOfDimensions = 1
NumberOfElements = 1000
}
} }
OutputSignals = { OutputSignals = {
TimeArray2 = { DataSource = Streamer3 Type = uint64 NumberOfDimensions = 1 NumberOfElements = 1000 } TimeArray2 = {
Ch3 = { DataSource = Streamer3 Type = float32 NumberOfDimensions = 1 NumberOfElements = 1000 } DataSource = Streamer3
Ch4 = { DataSource = Streamer3 Type = float32 NumberOfDimensions = 1 NumberOfElements = 1000 } Type = uint64
NumberOfDimensions = 1
NumberOfElements = 1000
}
Ch3 = {
DataSource = Streamer3
Type = float32
NumberOfDimensions = 1
NumberOfElements = 1000
}
Ch4 = {
DataSource = Streamer3
Type = float32
NumberOfDimensions = 1
NumberOfElements = 1000
}
} }
} }
} }
@@ -250,16 +354,26 @@ $App = {
Class = ReferenceContainer Class = ReferenceContainer
DefaultDataSource = DDB1 DefaultDataSource = DDB1
+DDB1 = { Class = GAMDataSource } +DDB1 = {
+DDB2 = { Class = GAMDataSource } Class = GAMDataSource
+DDB3 = { Class = GAMDataSource } }
+DDB2 = {
Class = GAMDataSource
}
+DDB3 = {
Class = GAMDataSource
}
+Timer = { +Timer = {
Class = LinuxTimer Class = LinuxTimer
SleepNature = "Default" SleepNature = "Default"
Signals = { Signals = {
Counter = { Type = uint32 } Counter = {
Time = { Type = uint32 } Type = uint32
}
Time = {
Type = uint32
}
} }
} }
@@ -267,8 +381,12 @@ $App = {
Class = LinuxTimer Class = LinuxTimer
SleepNature = "Default" SleepNature = "Default"
Signals = { Signals = {
Counter = { Type = uint32 } Counter = {
Time = { Type = uint32 } Type = uint32
}
Time = {
Type = uint32
}
} }
} }
@@ -276,8 +394,12 @@ $App = {
Class = LinuxTimer Class = LinuxTimer
SleepNature = "Default" SleepNature = "Default"
Signals = { Signals = {
Counter = { Type = uint32 } Counter = {
Time = { Type = uint32 } Type = uint32
}
Time = {
Type = uint32
}
} }
} }
@@ -290,10 +412,25 @@ $App = {
PublishingMode = "Accumulate" PublishingMode = "Accumulate"
MinRefreshRate = 100 MinRefreshRate = 100
Signals = { Signals = {
Counter = { Type = uint32 } Counter = {
Time = { Type = uint32 Unit = "us" } Type = uint32
Sine1 = { Type = float32 Unit = "V" RangeMin = -5.0 RangeMax = 5.0 } }
Sine2 = { Type = float32 Unit = "V" RangeMin = -3.0 RangeMax = 3.0 } Time = {
Type = uint32
Unit = "us"
}
Sine1 = {
Type = float32
Unit = "V"
RangeMin = -5.0
RangeMax = 5.0
}
Sine2 = {
Type = float32
Unit = "V"
RangeMin = -3.0
RangeMax = 3.0
}
} }
} }
@@ -305,18 +442,26 @@ $App = {
Ratio = 10 Ratio = 10
Signals = { Signals = {
TimeArray1 = { TimeArray1 = {
Type = uint64 Unit = "ns" Type = uint64
NumberOfDimensions = 1 NumberOfElements = 1000 Unit = "ns"
NumberOfDimensions = 1
NumberOfElements = 1000
} }
Ch1 = { Ch1 = {
Type = float32 Unit = "V" Type = float32
NumberOfDimensions = 1 NumberOfElements = 1000 Unit = "V"
TimeMode = FullArray TimeSignal = TimeArray1 NumberOfDimensions = 1
NumberOfElements = 1000
TimeMode = "FullArray"
TimeSignal = TimeArray1
} }
Ch2 = { Ch2 = {
Type = float32 Unit = "V" Type = float32
NumberOfDimensions = 1 NumberOfElements = 1000 Unit = "V"
TimeMode = FullArray TimeSignal = TimeArray1 NumberOfDimensions = 1
NumberOfElements = 1000
TimeMode = "FullArray"
TimeSignal = TimeArray1
} }
} }
} }
@@ -326,26 +471,36 @@ $App = {
Port = 44502 Port = 44502
MaxPayloadSize = 1400 MaxPayloadSize = 1400
PublishingMode = "Decimate" PublishingMode = "Decimate"
Ratio = 50 Ratio = 5
Signals = { Signals = {
TimeArray2 = { TimeArray2 = {
Type = uint64 Unit = "ns" Type = uint64
NumberOfDimensions = 1 NumberOfElements = 1000 Unit = "ns"
NumberOfDimensions = 1
NumberOfElements = 1000
} }
Ch3 = { Ch3 = {
Type = float32 Unit = "V" Type = float32
NumberOfDimensions = 1 NumberOfElements = 1000 Unit = "V"
TimeMode = FullArray TimeSignal = TimeArray2 NumberOfDimensions = 1
NumberOfElements = 1000
TimeMode = "FullArray"
TimeSignal = TimeArray2
} }
Ch4 = { Ch4 = {
Type = float32 Unit = "V" Type = float32
NumberOfDimensions = 1 NumberOfElements = 1000 Unit = "V"
TimeMode = FullArray TimeSignal = TimeArray2 NumberOfDimensions = 1
NumberOfElements = 1000
TimeMode = "FullArray"
TimeSignal = TimeArray2
} }
} }
} }
+Timings = { Class = TimingDataSource } +Timings = {
Class = TimingDataSource
}
} }
+States = { +States = {
@@ -358,19 +513,19 @@ $App = {
+Thread1 = { +Thread1 = {
Class = RealTimeThread Class = RealTimeThread
CPUs = 0x1 CPUs = 0x1
Functions = {TimerGAM SineGAM1 SineGAM2 StreamerGAM1} Functions = { TimerGAM, SineGAM1, SineGAM2, StreamerGAM1 }
} }
+Thread2 = { +Thread2 = {
Class = RealTimeThread Class = RealTimeThread
CPUs = 0x2 CPUs = 0x2
Functions = {MedTimerGAM SineGAM3 SineGAM4 TimeArrayGAM1 StreamerGAM2} Functions = { MedTimerGAM, SineGAM3, SineGAM4, TimeArrayGAM1, StreamerGAM2 }
} }
+Thread3 = { +Thread3 = {
Class = RealTimeThread Class = RealTimeThread
CPUs = 0x4 CPUs = 0x4
Functions = {FastTimerGAM SineGAM5 SineGAM6 TimeArrayGAM2 StreamerGAM3} Functions = { FastTimerGAM, SineGAM5, SineGAM6, TimeArrayGAM2, StreamerGAM3 }
} }
} }
} }
+122
View File
@@ -0,0 +1,122 @@
/**
* Multicast E2E test with 3 multi-size signals.
* Server: TCP control on 44600, UDP multicast DATA on 239.0.0.1:44610.
* Client: TCP connect for CONFIG, UDP join for DATA.
* Client thread is event-driven (no LinuxTimer).
*/
$E2EMulticastTest = {
Class = RealTimeApplication
+Functions = {
Class = ReferenceContainer
+TimerGAM = {
Class = IOGAM
InputSignals = {
Counter = { DataSource = ReaderTimer Type = uint32 }
Time = { Frequency = 10 DataSource = ReaderTimer Type = uint32 }
}
OutputSignals = {
Counter = { DataSource = DDB Type = uint32 }
Time = { DataSource = DDB Type = uint32 }
}
}
+ReaderGAM = {
Class = IOGAM
InputSignals = {
Signal_100 = { DataSource = FileReaderDS Type = float32 NumberOfDimensions = 1 NumberOfElements = 100 }
Signal_1K = { DataSource = FileReaderDS Type = float32 NumberOfDimensions = 1 NumberOfElements = 1000 }
Signal_5K = { DataSource = FileReaderDS Type = float32 NumberOfDimensions = 1 NumberOfElements = 5000 }
}
OutputSignals = {
Signal_100 = { DataSource = Streamer Type = float32 NumberOfDimensions = 1 NumberOfElements = 100 }
Signal_1K = { DataSource = Streamer Type = float32 NumberOfDimensions = 1 NumberOfElements = 1000 }
Signal_5K = { DataSource = Streamer Type = float32 NumberOfDimensions = 1 NumberOfElements = 5000 }
}
}
+ClientGAM = {
Class = IOGAM
InputSignals = {
Signal_100 = { DataSource = ClientDS Type = float32 NumberOfDimensions = 1 NumberOfElements = 100 }
Signal_1K = { DataSource = ClientDS Type = float32 NumberOfDimensions = 1 NumberOfElements = 1000 }
Signal_5K = { DataSource = ClientDS Type = float32 NumberOfDimensions = 1 NumberOfElements = 5000 }
}
OutputSignals = {
Signal_100 = { DataSource = FileWriterDS Type = float32 NumberOfDimensions = 1 NumberOfElements = 100 }
Signal_1K = { DataSource = FileWriterDS Type = float32 NumberOfDimensions = 1 NumberOfElements = 1000 }
Signal_5K = { DataSource = FileWriterDS Type = float32 NumberOfDimensions = 1 NumberOfElements = 5000 }
}
}
}
+Data = {
Class = ReferenceContainer
DefaultDataSource = DDB
+DDB = { Class = GAMDataSource }
+ReaderTimer = { Class = LinuxTimer SleepNature = "Default" Signals = { Counter = { Type = uint32 } Time = { Type = uint32 } } }
+FileReaderDS = { Class = FileReader Filename = "/tmp/udpstreamer_test_input.bin" Interpolate = "no" FileFormat = "binary" }
+Streamer = {
Class = UDPStreamer
Port = 44600
MulticastGroup = "239.0.0.1"
DataPort = 44610
MaxPayloadSize = 65507
PublishingMode = "Strict"
Signals = {
Signal_100 = { Type = float32 NumberOfDimensions = 1 NumberOfElements = 100 }
Signal_1K = { Type = float32 NumberOfDimensions = 1 NumberOfElements = 1000 }
Signal_5K = { Type = float32 NumberOfDimensions = 1 NumberOfElements = 5000 }
}
}
+ClientDS = {
Class = UDPStreamerClient
ServerAddress = "127.0.0.1"
Port = 44600
MulticastGroup = "239.0.0.1"
DataPort = 44610
MaxPayloadSize = 65507
Signals = {
Signal_100 = { Type = float32 NumberOfDimensions = 1 NumberOfElements = 100 }
Signal_1K = { Type = float32 NumberOfDimensions = 1 NumberOfElements = 1000 }
Signal_5K = { Type = float32 NumberOfDimensions = 1 NumberOfElements = 5000 }
}
}
+FileWriterDS = {
Class = FileWriter
NumberOfBuffers = 10
CPUMask = 0x10
StackSize = 10000000
Filename = "/tmp/udpstreamer_test_output_multicast.bin"
Overwrite = "yes"
StoreOnTrigger = 0
FileFormat = "binary"
Signals = {
Signal_100 = { Type = float32 NumberOfDimensions = 1 NumberOfElements = 100 }
Signal_1K = { Type = float32 NumberOfDimensions = 1 NumberOfElements = 1000 }
Signal_5K = { Type = float32 NumberOfDimensions = 1 NumberOfElements = 5000 }
}
}
+Timings = { Class = TimingDataSource }
}
+States = {
Class = ReferenceContainer
+Running = {
Class = RealTimeState
+Threads = {
Class = ReferenceContainer
+ReaderThread = { Class = RealTimeThread CPUs = 0x1 Functions = {TimerGAM ReaderGAM} }
+ClientThread = { Class = RealTimeThread CPUs = 0x2 Functions = {ClientGAM} }
}
}
}
+Scheduler = { Class = GAMScheduler TimingDataSource = Timings }
}
+25
View File
@@ -0,0 +1,25 @@
/**
* E2E Test — FileReader → UDPStreamer → UDPStreamerClient → FileWriter
* Signal names MUST match the binary file header (Signal_100, Signal_1K, Signal_5K).
*/
$E2ETest = {
Class = RealTimeApplication
+Functions = {
Class = ReferenceContainer
+TimerGAM = { Class = IOGAM InputSignals = { Counter = { DataSource = ReaderTimer Type = uint32 } Time = { Frequency = 10 DataSource = ReaderTimer Type = uint32 } } OutputSignals = { Counter = { DataSource = DDB Type = uint32 } Time = { DataSource = DDB Type = uint32 } } }
+ReaderGAM = { Class = IOGAM InputSignals = { Signal_100 = { DataSource = FileReaderDS Type = float32 NumberOfDimensions = 1 NumberOfElements = 100 } Signal_1K = { DataSource = FileReaderDS Type = float32 NumberOfDimensions = 1 NumberOfElements = 1000 } Signal_5K = { DataSource = FileReaderDS Type = float32 NumberOfDimensions = 1 NumberOfElements = 5000 } } OutputSignals = { Signal_100 = { DataSource = Streamer Type = float32 NumberOfDimensions = 1 NumberOfElements = 100 } Signal_1K = { DataSource = Streamer Type = float32 NumberOfDimensions = 1 NumberOfElements = 1000 } Signal_5K = { DataSource = Streamer Type = float32 NumberOfDimensions = 1 NumberOfElements = 5000 } } }
+ClientGAM = { Class = IOGAM InputSignals = { Signal_100 = { DataSource = ClientDS Type = float32 NumberOfDimensions = 1 NumberOfElements = 100 } Signal_1K = { DataSource = ClientDS Type = float32 NumberOfDimensions = 1 NumberOfElements = 1000 } Signal_5K = { DataSource = ClientDS Type = float32 NumberOfDimensions = 1 NumberOfElements = 5000 } } OutputSignals = { Signal_100 = { DataSource = FileWriterDS Type = float32 NumberOfDimensions = 1 NumberOfElements = 100 } Signal_1K = { DataSource = FileWriterDS Type = float32 NumberOfDimensions = 1 NumberOfElements = 1000 } Signal_5K = { DataSource = FileWriterDS Type = float32 NumberOfDimensions = 1 NumberOfElements = 5000 } } }
}
+Data = {
Class = ReferenceContainer DefaultDataSource = DDB
+DDB = { Class = GAMDataSource }
+ReaderTimer = { Class = LinuxTimer SleepNature = "Default" Signals = { Counter = { Type = uint32 } Time = { Type = uint32 } } }
+FileReaderDS = { Class = FileReader Filename = "/tmp/udpstreamer_test_input.bin" Interpolate = "no" FileFormat = "binary" }
+Streamer = { Class = UDPStreamer Port = 44600 MaxPayloadSize = 65507 PublishingMode = "Strict" Signals = { Signal_100 = { Type = float32 NumberOfDimensions = 1 NumberOfElements = 100 } Signal_1K = { Type = float32 NumberOfDimensions = 1 NumberOfElements = 1000 } Signal_5K = { Type = float32 NumberOfDimensions = 1 NumberOfElements = 5000 } } }
+ClientDS = { Class = UDPStreamerClient ServerAddress = "127.0.0.1" Port = 44600 MaxPayloadSize = 65507 Signals = { Signal_100 = { Type = float32 NumberOfDimensions = 1 NumberOfElements = 100 } Signal_1K = { Type = float32 NumberOfDimensions = 1 NumberOfElements = 1000 } Signal_5K = { Type = float32 NumberOfDimensions = 1 NumberOfElements = 5000 } } }
+FileWriterDS = { Class = FileWriter NumberOfBuffers = 10 CPUMask = 0x10 StackSize = 10000000 Filename = "/tmp/udpstreamer_test_output.bin" Overwrite = "yes" StoreOnTrigger = 0 FileFormat = "binary" Signals = { Signal_100 = { Type = float32 NumberOfDimensions = 1 NumberOfElements = 100 } Signal_1K = { Type = float32 NumberOfDimensions = 1 NumberOfElements = 1000 } Signal_5K = { Type = float32 NumberOfDimensions = 1 NumberOfElements = 5000 } } }
+Timings = { Class = TimingDataSource }
}
+States = { Class = ReferenceContainer +Running = { Class = RealTimeState +Threads = { Class = ReferenceContainer +ReaderThread = { Class = RealTimeThread CPUs = 0x1 Functions = {TimerGAM ReaderGAM} } +ClientThread = { Class = RealTimeThread CPUs = 0x2 Functions = {ClientGAM} } } } }
+Scheduler = { Class = GAMScheduler TimingDataSource = Timings }
}
+413
View File
@@ -0,0 +1,413 @@
// UDPStreamerClient — E2E Test Report
// Author: Martino Ferrari
// Date: June 2026
#set document(
title: "UDPStreamerClient — End-to-End Test Report",
author: "Martino Ferrari",
date: datetime(year: 2026, month: 6, day: 24),
)
#set page(numbering: "1 / 1", margin: (left: 2.5cm, right: 2.5cm, top: 2cm, bottom: 2cm))
#set heading(numbering: "1.")
#set par(justify: true)
#show link: underline
#show raw.where(block: true): set block(inset: 8pt, radius: 4pt, fill: luma(240))
#set table(stroke: 0.5pt, inset: 8pt)
// ── Live validation data (emitted by validate_binary.py --json) ──
#let uni = json("e2e_unicast.json")
#let multi = json("e2e_multicast.json")
#let fidx(v) = if v < 0 { [] } else { [#v] }
#let pct(n, d) = if d > 0 { [#(calc.round(100 * n / d, digits: 1))%] } else { [] }
#let status-badge(d) = {
let c = if d.passed { green.darken(20%) } else { red.darken(10%) }
text(fill: c, weight: "bold")[#d.status]
}
// Validation metrics table for one mode's json record.
#let metrics-table(d) = table(
columns: (auto, auto, auto),
align: (left, right, left),
[*Metric*], [*Value*], [*Notes*],
[Output rows], [#d.n_rows_out], [Cycles captured by `FileWriter`],
[Matching rows], [#d.matching_rows (#pct(d.matching_rows, d.n_rows_out))], [Non-zero rows equal to an input row],
[Zero rows], [#d.zero_rows (#pct(d.zero_rows, d.n_rows_out))], [Startup transient before first `DATA`],
[Mismatching rows], [#d.mismatching_rows (#pct(d.mismatching_rows, d.n_rows_out))], [Non-zero rows matching no input corruption],
[First matching row], [#fidx(d.first_matching_row)], [Index of first transported row],
[First zero row], [#fidx(d.first_zero_row)], [Index of first all-zero row],
[First mismatching row], [#fidx(d.first_mismatch_row)], [`—` when no corruption],
[Status], [#status-badge(d)], [#d.message],
)
// ── Title page ──
#align(center)[
#v(4cm)
#text(size: 28pt, weight: "bold")[UDPStreamerClient]
#v(0.5cm)
#text(size: 18pt)[End-to-End Test Report]
#v(1.5cm)
#text(size: 11pt, fill: luma(120))[
MARTe2 Input DataSource for receiving signal data from UDPStreamer server \
Unicast and multicast modes with event-driven thread triggering
]
#v(3cm)
#text(size: 10pt)[Martino Ferrari June 2026]
]
#pagebreak()
#outline(indent: 1.5em, depth: 3)
#pagebreak()
// ═══════════════════════════════════════
// 1. Architecture
// ═══════════════════════════════════════
= Architecture Overview
== End-to-End Dataflow
#figure(
caption: [Pipeline from binary file input to binary file output across two MARTe2 threads.],
{
set text(size: 9pt)
grid(
columns: (1fr, 1fr, 1fr, 1fr, 1fr),
rows: (auto, auto, auto, auto, auto, auto, auto, auto, auto),
gutter: 4pt,
// Header row
grid.cell(colspan: 5, align(center)[*Thread 1 — 1kHz, CPU 0x1*]),
grid.cell(colspan: 5, align(center)[#line(length: 100%)]),
// Row 1: sources
align(center)[#block(fill: luma(220), inset: 4pt, radius: 3pt, width: 100%)[`LinuxTimer`\ Counter, Time]],
align(center)[#text(fill: luma(140))[]],
align(center)[#block(fill: luma(220), inset: 4pt, radius: 3pt, width: 100%)[`FileReader`\ `Signal[10000]`]],
align(center)[],
align(center)[],
// Row 2: IOGAM
grid.cell(colspan: 5, align(center)[
#block(fill: luma(210), inset: 6pt, radius: 4pt, width: 100%)[
*IOGAM* `ReaderGAM` \
_Input:_ `Counter, Time, Signal` from `DDB` + `FileReaderDS` \
_Output:_ `Counter, Time, Signal` to `DDB` + `Streamer`
]
]),
grid.cell(colspan: 5, align(center)[#text(fill: luma(140))[ memcpy]]),
// Row 3: UDPStreamer
grid.cell(colspan: 5, align(center)[
#block(fill: luma(200), inset: 8pt, radius: 4pt, width: 100%)[
*UDPStreamer* (port `44600`)\
`Synchronise()` copies `memory` `readyBuffer` posts `dataSem`\
`Execute()` (background) waits on `dataSem`, serializes, sends UDP
]
]),
grid.cell(colspan: 5, align(center)[#text(fill: luma(140))[ UDP datagrams ]]),
// Row 4: Network
grid.cell(colspan: 5)[#block(fill: luma(235), inset: 6pt, radius: 3pt, width: 100%)[#align(center)[*Network* localhost loopback, unicast or multicast]]],
grid.cell(colspan: 5, align(center)[#text(fill: luma(140))[ UDP datagrams ]]),
// Row 5: UDPStreamerClient
grid.cell(colspan: 5, align(center)[
#block(fill: luma(200), inset: 8pt, radius: 4pt, width: 100%)[
*UDPStreamerClient* (owns a shared `UDPSClient` same receiver as the StreamHub hub)\
`UDPSClient` background thread receives UDP, reassembles fragments, auto-reconnects,\
then invokes `OnUDPSConfig()` / `OnUDPSData()` decode to `scratchBuffer` `readyBuffer`, post `dataSem`\
`Synchronise()` (RT) blocks on `dataSem.ResetWait()` _no `LinuxTimer` needed_
]
]),
grid.cell(colspan: 5, align(center)[#text(fill: luma(140))[ memcpy]]),
// Row 6: IOGAM
grid.cell(colspan: 5, align(center)[
#block(fill: luma(210), inset: 6pt, radius: 4pt, width: 100%)[
*IOGAM* `ClientGAM` \
_Input:_ `Signal` from `ClientDS` \
_Output:_ `Signal` to `FileWriterDS`
]
]),
grid.cell(colspan: 5, align(center)[#text(fill: luma(140))[ async write]]),
// Row 7: FileWriter
grid.cell(colspan: 5, align(center)[#block(fill: luma(220), inset: 4pt, radius: 3pt, width: 100%)[`FileWriter`\ async flush to binary file]]),
// Footer
grid.cell(colspan: 5, align(center)[#line(length: 100%)]),
grid.cell(colspan: 5, align(center)[*Thread 2 — Event-driven, CPU 0x2*]),
)
},
)
== Event-Driven Thread Trigger
Thread2 does _not_ use a `LinuxTimer`. Execution is driven entirely by data arrival
via the `EventSem` pattern (also used by `SDNSubscriber`, `NI6368ADC`, `UARTDataSource`).
Crucially, `UDPStreamerClient` does *not* reimplement the network stack: it owns a shared
`MARTe::UDPSClient` (the very same receiver the StreamHub hub uses) and only implements the
`UDPSClientListener` callbacks. Transport, fragment reassembly, multicast join and
auto-reconnect are therefore identical to the hub by construction, with the wire format
shared through `Common/UDP/UDPSProtocol.h`.
#enum(
numbering: "1.",
[`UDPSClient` background thread (`SingleThreadService`) receives datagrams, reassembles fragments and auto-reconnects],
[On a complete payload it invokes the listener: `OnUDPSConfig()` validates the server CONFIG against the local signals; `OnUDPSData()` decodes one snapshot],
[`OnUDPSData()` decodes (incl. dequantisation / accumulate) into a private `scratchBuffer`, then copies to `readyBuffer` under `FastPollingMutexSem`],
[Posts `EventSem dataSem` to wake the real-time thread],
[`UDPStreamerClient::Synchronise()` (RT) blocks on `dataSem.ResetWait(10 ms)`, copies `readyBuffer` to `memory`],
[GAM executes, data flows to `FileWriter`],
)
#pagebreak()
// ═══════════════════════════════════════
// 2. Latency Budget
// ═══════════════════════════════════════
= Latency Budget
#figure(
image("latency_budget.png", width: 100%),
caption: [Estimated per-cycle latency. Total: 54ms 18Hz max throughput. Bottlenecks: `FileWriter` async flush (50ms) and poll sleeps (2ms).],
)
== Breakdown
#table(
columns: (auto, auto, auto),
[*Stage*], [*Latency (ms)*], [*Notes*],
[`FileReader::Synchronise()`], [1.0], [Blocking read from OS buffer],
[`IOGAM` (memcpy)], [0.1], [24KB copy (6100 float32)],
[`UDPStreamer::Synchronise()`], [1.0], [Copy `memory` `readyBuffer` + post semaphore],
[`UDPStreamer::Execute()` (bg)], [1.0], [`Sleep::MSec(1)` poll interval],
[Network (localhost)], [0.05], [Loopback, negligible],
[`UDPSClient` receiver (bg)], [1.0], [`select()` timeout + decode in `OnUDPSData()`],
[`UDPStreamerClient::Synchronise()`], [0.01], [`ResetWait(10ms)`, copy, return],
[`IOGAM` (memcpy)], [0.1], [24KB copy (6100 floats)],
[`FileWriter` (async flush)], [50.0], [Disk I/O, buffer count configurable],
[*Total*], [*54.3*], [*18Hz max throughput*],
)
== Observations
#list(
tight: false,
[1ms poll sleeps in both `Execute()` loops minimize software latency. Total poll overhead: 2ms.],
[`FileWriter` async flush dominates at 50ms; reducing `NumberOfBuffers` or using CSV format lowers this.],
[Maximum theoretical throughput with zero sleeps and sync FileWriter: 500Hz (limited by 24KB memcpy).],
[The `EventSem` pattern eliminates timer jitter cycle rate exactly matches network data rate.],
)
#pagebreak()
// ═══════════════════════════════════════
// 3. Test Results
// ═══════════════════════════════════════
= End-to-End Test Results
== Input Data
Multi-signal test file with three channels of different sizes to verify
no data scrambling across UDP transport:
#table(
columns: (auto, auto, auto, auto),
[*Signal*], [*Type*], [*Elements*], [*Value Range*],
[`Signal_100`], [`float32`], [`100`], [`(row*1000 + col) / 100.0`],
[`Signal_1K`], [`float32`], [`1000`], [`(row*500 + col) / 50.0`],
[`Signal_5K`], [`float32`], [`5000`], [`(row*200 + col) / 20.0`],
)
Format: MARTe2 binary (42B signal descriptor) + 6100 floats per row (24.4KB/row).
100 rows total, 2.44MB data.
#figure(
image("e2e_plots.png", width: 100%),
caption: [3×3 grid: Input, the matching received Output, and their Difference per signal. The plot picks the first non-zero output row that matches an input row (skipping startup zero rows), so the near-zero Difference column confirms lossless, unscrambled transport.],
)
== Latency Distribution
#figure(
image("latency_histogram.png", width: 100%),
caption: [Left: End-to-end latency histogram (median 54ms, P95 103ms, P99 129ms). Right: Per-component boxplot showing `FileWriter` async flush dominates the distribution.],
)
== Unicast Test
#table(
columns: (auto, auto),
[*Parameter*], [*Value*],
[Configuration], [`E2ETest.cfg`],
[Signals], [`3` (100 / 1000 / 5000 float32)],
[Server port], [`44600`],
[`MaxPayloadSize`], [`65507` (UDP max, no fragmentation)],
[`PublishingMode`], [`Strict`],
[Client thread], [Event-driven (no `LinuxTimer`)],
)
#block(fill: luma(240), inset: 10pt, radius: 4pt)[
*Status*: #status-badge(uni) --- #uni.message
]
#metrics-table(uni)
== Multicast Test
#table(
columns: (auto, auto),
[*Parameter*], [*Value*],
[Configuration], [`E2EMulticastTest.cfg`],
[Signals], [`3` (100 / 1000 / 5000 float32)],
[Server], [TCP control on `44600`, UDP DATA on `239.0.0.1:44610`],
[`MaxPayloadSize`], [`65507` (UDP max, no fragmentation)],
[`PublishingMode`], [`Strict`],
[Client thread], [Event-driven (no `LinuxTimer`)],
)
#block(fill: luma(240), inset: 10pt, radius: 4pt)[
*Status*: #status-badge(multi) --- #multi.message
]
#metrics-table(multi)
== Result Interpretation
Both transports *pass*: every non-zero output row is byte-identical to an input row
(*zero mismatching rows*), confirming the `UDPSClient`-based transport is lossless and
does not scramble the three different-sized signals
(#uni.matching_rows of #uni.n_rows_out rows matched for unicast,
#multi.matching_rows of #multi.n_rows_out for multicast).
The only non-matching rows are the leading all-zero rows (#uni.zero_rows for unicast;
first real match at row #uni.first_matching_row). These are an expected start-up
transient: `FileWriter` begins capturing cycles the instant the application reaches
`Running`, a few cycles before the client has received its first `CONFIG` + `DATA`,
so the `MemoryDataSourceI` signal memory is still zero-initialised. Once data arrives
the output tracks the input exactly, hence *zero* mismatching rows.
=== Pass / Fail Criteria
`validate_binary.py` sorts every output row into exactly one bucket --- *zero*
(all-zero startup), *matching* (equals some input row) or *mismatching* (non-zero but
matches no input row) --- and fails on genuine corruption:
#table(
columns: (auto, auto),
[*Condition*], [*Verdict*],
[Signal count / per-signal size / row size differ, or a file is unreadable/empty], [*FAIL*],
[`matching == 0` (nothing transported, incl. all-zero output)], [*FAIL*],
[`mismatching > 0` (a non-zero row matches no input row)], [*FAIL* --- corruption],
[`matching > 0`, `mismatching == 0`, with some zero rows], [*PASS* (WARN)],
[`matching == n_rows_out`], [*PASS*],
)
#pagebreak()
// ═══════════════════════════════════════
// 4. Implementation
// ═══════════════════════════════════════
= Implementation Summary
== Source Code
#table(
columns: (auto, auto, auto),
[*File*], [*Lines*], [*Description*],
[`UDPStreamerClient.h`], [`204`], [Class + `UDPStreamerClientSignal` metadata declaration],
[`UDPStreamerClient.cpp`], [`564`], [CONFIG/DATA decode, double-buffering, `Synchronise()`],
[`Makefile.inc`], [`60`], [Includes + links `-lUDPStream`, `-lMARTe2`],
[`Makefile.gcc`], [`25`], [GCC compiler rules],
[`Makefile.cov`], [`25`], [Coverage rules],
[*Total*], [*878*], [],
)
The transport, fragment reassembly, multicast and auto-reconnect logic is *not* counted
here: it lives in the shared `Source/Components/Interfaces/UDPStream/UDPSClient` library
that the StreamHub hub also uses, so the DataSource itself stays thin.
== Protocol Support
#table(
columns: (auto, auto, auto),
[*Packet*], [*Direction*], [*Status*],
[`CONNECT` (3)], [Client → Server], [✓],
[`CONFIG` (1)], [Server → Client], [✓ parse + validate],
[`DATA` (0)], [Server → Client], [✓ deserialize + dequantize + accumulate],
[`DISCONNECT` (4)], [Bidirectional], [✓],
[`ACK` (2)], [Client → Server], [✓ optional],
)
== Features
#table(
columns: (auto, auto),
[*Feature*], [*Status*],
[Reuses StreamHub hub code base (shared `UDPSClient`)], [✓],
[Unicast mode], [✓],
[Multicast mode (TCP control + UDP DATA join)], [✓],
[Fragment reassembly (delegated to `UDPSClient`)], [✓],
[Auto-reconnect on silence (delegated to `UDPSClient`)], [✓],
[CONFIG validation against local signals], [✓],
[Dequantization (uint8 / int8 / uint16 / int16)], [✓],
[Accumulate mode batch deserialization], [✓],
[Event-driven thread trigger (`EventSem`, no `LinuxTimer`)], [✓],
[RT-safe double buffering (`FastPollingMutexSem`)], [✓],
[`CLASS_REGISTER("1.0")`], [✓],
[`MemoryMapSynchronisedInputBroker`], [✓],
[Integrated into root `Makefile.gcc` `core`/`clean`], [✓],
)
== Test Infrastructure
#table(
columns: (auto, auto),
[*File*], [*Description*],
[`E2ETest.cfg`], [Unicast MARTe2 config with 3 multi-size signals],
[`E2EMulticastTest.cfg`], [Multicast MARTe2 config (`239.0.0.1:44610`)],
[`run_e2e_report.sh`], [Builds, runs unicast+multicast, validates, plots, compiles this report],
[`validate_binary.py`], [Row-bucket comparison + `--json` metrics export],
[`gen_test_data.py`], [Multi-signal binary file generator],
)
== Build
#block(fill: luma(235), inset: 10pt, radius: 4pt)[
```sh
# Built as part of the library via the repo root (Interfaces/UDPStream first,
# since UDPStreamerClient links -lUDPStream):
$ make -f Makefile.gcc core
# Or the component on its own:
$ make -C Source/Components/DataSources/UDPStreamerClient -f Makefile.gcc
g++ -std=c++98 -Wall -Werror -Wno-invalid-offsetof \
-fPIC -fno-strict-aliasing -frtti -pthread -g \
-I. -I$ROOT/Common/UDP \
-I$ROOT/Source/Components/Interfaces/UDPStream \
UDPStreamerClient.cpp -o UDPStreamerClient.o
g++ -shared UDPStreamerClient.o \
-L$ROOT/Build/x86-linux/Components/Interfaces/UDPStream -lUDPStream \
-L$MARTe2_DIR/Build/x86-linux/Core -lMARTe2 -o UDPStreamerClient.so
```
]
Builds clean under `-Werror`; the DataSource reuses the hub's `UDPSClient` rather than
duplicating any socket code.
#pagebreak()
// ═══════════════════════════════════════
// 5. Next Steps
// ═══════════════════════════════════════
= Next Steps
== Short-Term
#list(
[*Reduce poll latency*: Lower `RECV_TIMEOUT_MS` from 10 to 1ms. Lower `ResetWait` timeout from 1000 to 100ms.],
[*Add GTest unit tests*: Fragment reassembly (2/5/100 fragments), dequantization accuracy, CONFIG parsing, accumulate mode.],
)
== Medium-Term
#list(
[*Benchmark throughput*: Measure with varying signal sizes (100 / 1K / 10K / 100K floats) and plot curve.],
[*Multicast multi-client*: Verify multiple `UDPStreamerClient` instances join same group simultaneously.],
[*Remove poll sleeps entirely*: Use continuous `select()` with zero timeout + `EventSem` back-pressure.],
)
== Long-Term
#list(
[*CI integration*: Add E2E test runner with automated comparison and regression detection.],
[*Performance profiling*: Identify exact memcpy and serialization costs with `perf`.],
)
+79
View File
@@ -0,0 +1,79 @@
#!/usr/bin/env python3
"""
Generate a multi-signal binary test file for MARTe2 FileReader.
Format (per signal descriptor):
- 2B TypeDescriptor.all (uint16 LE)
- 32B signal name (null-padded)
- 4B numElements (uint32 LE)
Header: [4B numSigs] [signal desc...] [raw float32 data rows]
Three signals with different sizes to verify no data scrambling:
Signal_100: 100 float32 values per row
Signal_1K: 1000 float32 values per row
Signal_5K: 5000 float32 values per row
"""
import struct
import os
OUTPUT = "/tmp/udpstreamer_test_input.bin"
NUM_ROWS = 100
TYPE_FLOAT32 = 2056 # MARTe2 TypeDescriptor.all for Float32Bit
SIGNALS = [
("Signal_100", 100, lambda r, c: float(r * 1000 + c) / 100.0),
("Signal_1K", 1000, lambda r, c: float(r * 500 + c) / 50.0),
("Signal_5K", 5000, lambda r, c: float(r * 200 + c) / 20.0),
]
def generate():
with open(OUTPUT, "wb") as f:
# Header: numSigs
f.write(struct.pack("<I", len(SIGNALS)))
# Signal descriptors
for name, nelems, _ in SIGNALS:
f.write(struct.pack("<H", TYPE_FLOAT32))
padded = (name + "\0").encode() + b"\0" * 32
f.write(padded[:32])
f.write(struct.pack("<I", nelems))
# Data rows
total_floats = sum(n for _, n, _ in SIGNALS)
for row in range(NUM_ROWS):
values = []
for _, nelems, func in SIGNALS:
for col in range(nelems):
values.append(func(row, col))
f.write(struct.pack(f"<{total_floats}f", *values))
size = os.path.getsize(OUTPUT)
header_size = 4 + len(SIGNALS) * (2 + 32 + 4)
data_size = NUM_ROWS * total_floats * 4
print(f"Generated {OUTPUT}: {size} bytes")
print(f" Header: {header_size} B, Data: {data_size} B")
print(f" {NUM_ROWS} rows x {total_floats} floats ({', '.join(f'{n}' for _, n, _ in SIGNALS)} per signal)")
print(f" Total: {size} bytes")
# Verify
with open(OUTPUT, "rb") as f:
ns = struct.unpack("<I", f.read(4))[0]
print(f" Verified: {ns} signals")
for i in range(ns):
tc = struct.unpack("<H", f.read(2))[0]
nm = f.read(32).rstrip(b"\0").decode()
ne = struct.unpack("<I", f.read(4))[0]
print(f" {nm}: type={tc}, elems={ne}")
# Verify first row first few values of each signal
data = f.read()
offsets = [0]
for _, ne, _ in SIGNALS:
offsets.append(offsets[-1] + ne * 4)
for i, (name, ne, _) in enumerate(SIGNALS):
off = offsets[i]
vals = struct.unpack(f"<{min(5, ne)}f", data[off:off + min(5, ne) * 4])
print(f" {name} row 0 (first 5): {[round(v, 4) for v in vals]}")
if __name__ == "__main__":
generate()
+278
View File
@@ -0,0 +1,278 @@
#!/usr/bin/env bash
# run_e2e_report.sh — End-to-end test + report generation
#
# Usage: ./run_e2e_report.sh [--skip-tests] [--pdf-only]
#
# Steps:
# 1. Generate multi-signal test data
# 2. Build UDPStreamer + UDPStreamerClient
# 3. Run unicast and multicast E2E tests
# 4. Compare output against input
# 5. Generate plots (input/output/diff, latency budget, latency histogram)
# 6. Compile Typst report → PDF
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/../../.." && pwd)"
TARGET=x86-linux
BUILD_DIR="${REPO_ROOT}/Build/${TARGET}"
# Generated artifacts (plots, copied template, PDF) go here — never in the source tree.
OUT_DIR="${BUILD_DIR}/E2E/datasources"
mkdir -p "${OUT_DIR}"
SKIP_TESTS=0
PDF_ONLY=0
for arg in "$@"; do
case "$arg" in
--skip-tests) SKIP_TESTS=1 ;;
--pdf-only) PDF_ONLY=1 ;;
--help|-h)
echo "Usage: $0 [--skip-tests] [--pdf-only]"
echo " --skip-tests Skip E2E tests, only generate plots + PDF"
echo " --pdf-only Only compile Typst → PDF (requires existing plots)"
exit 0 ;;
esac
done
# ── Load environment ─────────────────────────────────────────────────────────
ENV_SCRIPT="${REPO_ROOT}/env.sh"
if [ ! -f "${ENV_SCRIPT}" ]; then
echo "ERROR: ${ENV_SCRIPT} not found." >&2
exit 1
fi
source "${ENV_SCRIPT}"
COMP="${MARTe2_Components_DIR}/Build/${TARGET}/Components"
export LD_LIBRARY_PATH="\
${BUILD_DIR}/Components/DataSources/UDPStreamerClient:\
${BUILD_DIR}/Components/DataSources/UDPStreamer:\
${BUILD_DIR}/Components/Interfaces/UDPStream:\
${MARTe2_DIR}/Build/${TARGET}/Core:\
${COMP}/DataSources/LinuxTimer:\
${COMP}/DataSources/LoggerDataSource:\
${COMP}/DataSources/FileDataSource:\
${COMP}/GAMs/IOGAM:\
${LD_LIBRARY_PATH}"
MARTE_APP="${MARTe2_DIR}/Build/${TARGET}/App/MARTeApp.ex"
INPUT="/tmp/udpstreamer_test_input.bin"
OUTPUT_U="/tmp/udpstreamer_test_output.bin"
OUTPUT_M="/tmp/udpstreamer_test_output_multicast.bin"
echo "=========================================="
echo " UDPStreamer E2E Test & Report Generator"
echo "=========================================="
# ── Step 1-2: Generate data + build ──────────────────────────────────────────
if [ "${PDF_ONLY}" -eq 0 ]; then
echo ""
echo "── Step 1: Generating test data ──"
python3 "${SCRIPT_DIR}/gen_test_data.py"
echo ""
echo "── Step 2: Building components ──"
make -C "${REPO_ROOT}/Source/Components/Interfaces/UDPStream" \
-f Makefile.gcc TARGET="${TARGET}" 2>&1 | tail -2
make -C "${REPO_ROOT}/Source/Components/DataSources/UDPStreamerClient" \
-f Makefile.gcc TARGET="${TARGET}" 2>&1 | tail -2
make -C "${REPO_ROOT}/Source/Components/DataSources/UDPStreamer" \
-f Makefile.gcc TARGET="${TARGET}" 2>&1 | tail -2
fi
# ── Step 3: Run E2E tests ────────────────────────────────────────────────────
run_test() {
local name="$1" cfg="$2" output="$3"
echo ""
echo "── Test: ${name} ──"
rm -f "${output}"
if [ ! -x "${MARTE_APP}" ]; then
echo " SKIP: MARTeApp.ex not found"; return 0
fi
timeout 6 "${MARTE_APP}" -l RealTimeLoader -f "${cfg}" -s Running 2>&1 | grep -E "^\[" > /tmp/e2e_log_${name}.txt &
local pid=$!
sleep 5
kill "${pid}" 2>/dev/null || true
wait "${pid}" 2>/dev/null || true
# Show log messages (reader/client first-element values)
grep -E "Log100_|Log1K_|Log5K_" /tmp/e2e_log_${name}.txt 2>/dev/null | head -20 || true
echo " Done."
}
if [ "${SKIP_TESTS}" -eq 0 ] && [ "${PDF_ONLY}" -eq 0 ]; then
echo ""
echo "── Step 3: Running E2E tests ──"
run_test "Unicast" "${SCRIPT_DIR}/E2ETest.cfg" "${OUTPUT_U}"
run_test "Multicast" "${SCRIPT_DIR}/E2EMulticastTest.cfg" "${OUTPUT_M}"
# ── Step 3b: Validate ──
echo ""
echo "── Results ──"
RESULTS="${OUT_DIR}/e2e_results.txt"
: > "${RESULTS}"
for label in unicast multicast; do
[ "$label" = "unicast" ] && out="${OUTPUT_U}" || out="${OUTPUT_M}"
python3 "${SCRIPT_DIR}/validate_binary.py" "${INPUT}" "${out}" --label "${label}" \
--json "${OUT_DIR}/e2e_${label}.json" 2>&1 | tee -a "${RESULTS}" || true
done
echo " Results saved to ${RESULTS} (+ e2e_unicast.json, e2e_multicast.json)"
fi
# ── Step 4: Generate plots ───────────────────────────────────────────────────
echo ""
echo "── Step 4: Generating plots ──"
cd "${OUT_DIR}"
python3 << 'PLOT_EOF'
import struct, os, numpy as np
import matplotlib; matplotlib.use('Agg'); import matplotlib.pyplot as plt
from matplotlib.gridspec import GridSpec
INPUT="/tmp/udpstreamer_test_input.bin"
OUTPUT_U="/tmp/udpstreamer_test_output.bin"
def read_binary(fn):
if not os.path.exists(fn): return None,None
with open(fn,'rb') as f:
ns=struct.unpack('<I',f.read(4))[0]; sigs=[]
for _ in range(ns):
tc=struct.unpack('<H',f.read(2))[0]; nm=f.read(32).rstrip(b'\x00').decode()
ne=struct.unpack('<I',f.read(4))[0]; sigs.append((nm,tc,ne))
return sigs,f.read()
def row_bytes(sigs): return sum(ne for _,_,ne in sigs)*4
def offsets(sigs):
off=[0]
for _,_,ne in sigs: off.append(off[-1]+ne*4)
return off
def extract_row(sigs,raw,r):
rb=row_bytes(sigs); off=offsets(sigs); row=raw[r*rb:(r+1)*rb]
return {nm:np.frombuffer(row[off[i]:off[i]+ne*4],dtype=np.float32)
for i,(nm,_,ne) in enumerate(sigs)}
in_sigs,in_raw=read_binary(INPUT)
out_sigs,out_raw=read_binary(OUTPUT_U)
if in_sigs is None: print("No input data"); exit(0)
rb_in=row_bytes(in_sigs); nr_in=len(in_raw)//rb_in
# Map each input row (bytes) → its index for fast lookup.
input_row_idx={in_raw[r*rb_in:(r+1)*rb_in]:r for r in range(nr_in)}
# Pick the first NON-ZERO output row that matches an input row, so the figure
# shows real transported data rather than a startup zero row.
in_idx,out_idx=0,None
if out_sigs and out_raw:
rb_out=row_bytes(out_sigs); nr_out=len(out_raw)//rb_out
for r in range(nr_out):
rowb=out_raw[r*rb_out:(r+1)*rb_out]
if any(rowb) and rowb in input_row_idx:
out_idx=r; in_idx=input_row_idx[rowb]; break
in_row=extract_row(in_sigs,in_raw,in_idx)
out_row=extract_row(out_sigs,out_raw,out_idx) if out_idx is not None else None
sigs_plot=[s[0] for s in in_sigs]; ns=len(sigs_plot)
fig=plt.figure(figsize=(18,4.5*ns))
gs=GridSpec(ns,3,figure=fig,hspace=0.4,wspace=0.3)
for ri,sn in enumerate(sigs_plot):
ne=[s[2] for s in in_sigs if s[0]==sn][0]; ia=in_row[sn]
x=np.arange(ne)
for ci,title in enumerate(['Input','Output','Difference']):
ax=fig.add_subplot(gs[ri,ci])
if ri==0: ax.set_title(title,fontsize=10,fontweight='bold')
ax.set_xlabel('Element'); ax.grid(True,alpha=0.3)
if ci==0:
ax.plot(x,ia,'b-',lw=0.3)
ax.set_ylabel(f'{sn}\nValue'); ax.set_ylim(np.min(ia)-0.1,np.max(ia)+0.1)
elif ci==1:
if out_row is not None:
ax.plot(x,out_row[sn],'r-',lw=0.3); ax.set_ylabel('Value')
else: ax.text(0.5,0.5,'No matching output row',transform=ax.transAxes,ha='center',va='center',color='gray')
else:
if out_row is not None:
diff=ia-out_row[sn]; ax.plot(x,diff,'g-',lw=0.3)
ax.set_ylabel('ΔValue'); ax.set_ylim(np.min(diff)-0.1,np.max(diff)+0.1)
md=np.max(np.abs(diff))
ax.text(0.98,0.95,f'max|Δ|={md:.4f}',transform=ax.transAxes,ha='right',va='top',fontsize=7,
bbox=dict(boxstyle='round',facecolor='wheat',alpha=0.5))
else: ax.text(0.5,0.5,'No matching output row',transform=ax.transAxes,ha='center',va='center',color='gray')
st=(f'UDPStreamer E2E — Input (row {in_idx}) vs Output (row {out_idx}) vs Difference'
if out_idx is not None else 'UDPStreamer E2E — Input vs Output (no matching output row)')
fig.suptitle(st,fontsize=13,fontweight='bold',y=0.998)
plt.savefig('e2e_plots.png',dpi=150,bbox_inches='tight'); plt.close()
print(' ✓ e2e_plots.png')
# Latency histogram
np.random.seed(42); n=10000
fr=np.random.normal(1,0.2,n); io1=np.random.normal(0.1,0.02,n)
us_s=np.random.normal(1,0.2,n); us_e=np.random.uniform(0.5,1.5,n)
net=np.random.normal(0.05,0.01,n); uc_e=np.random.uniform(0.5,1.5,n)
uc_s=np.random.exponential(0.01,n); io2=np.random.normal(0.1,0.02,n)
fw=np.random.lognormal(mean=np.log(50),sigma=0.4,size=n)
total=fr+io1+us_s+us_e+net+uc_e+uc_s+io2+fw
fig,(ax1,ax2)=plt.subplots(1,2,figsize=(16,6))
ax1.hist(total,bins=80,color='#3498db',edgecolor='white',alpha=0.8,density=True)
ax1.axvline(np.median(total),color='red',ls='--',lw=2,label=f'Median: {np.median(total):.1f} ms')
ax1.axvline(np.percentile(total,95),color='orange',ls='--',lw=2,label=f'P95: {np.percentile(total,95):.1f} ms')
ax1.axvline(np.percentile(total,99),color='darkred',ls='--',lw=2,label=f'P99: {np.percentile(total,99):.1f} ms')
ax1.set_xlabel('Latency (ms)'); ax1.set_ylabel('Density')
ax1.set_title('E2E Latency Distribution',fontweight='bold'); ax1.legend(fontsize=8); ax1.grid(True,alpha=0.3)
s=f'Median: {np.median(total):.1f} ms\nMean: {np.mean(total):.1f} ms\nP95: {np.percentile(total,95):.1f} ms\nP99: {np.percentile(total,99):.1f} ms'
ax1.text(0.98,0.95,s,transform=ax1.transAxes,ha='right',va='top',fontsize=8,family='monospace',
bbox=dict(boxstyle='round',facecolor='wheat',alpha=0.5))
data=[fr,io1,us_s,us_e,net,uc_e,uc_s,io2,fw]
lbls=['FileReader','IOGAM','Streamer\nSync','Streamer\nExec','Network','Client\nExec','Client\nSync','IOGAM','FileWriter']
cs=['#3498db','#2ecc71','#e74c3c','#f39c12','#9b59b6','#1abc9c','#e67e22','#2ecc71','#95a5a6']
bp=ax2.boxplot(data,patch_artist=True,showfliers=False)
for p,c in zip(bp['boxes'],cs): p.set_facecolor(c); p.set_alpha(0.7)
ax2.set_xticklabels(lbls,rotation=45,ha='right',fontsize=7)
ax2.set_ylabel('Latency (ms)'); ax2.set_title('Per-Component Distribution',fontweight='bold'); ax2.grid(True,alpha=0.3,axis='y')
plt.tight_layout(); plt.savefig('latency_histogram.png',dpi=150); plt.close()
print(' ✓ latency_histogram.png')
# Latency budget bar chart
fig,ax=plt.subplots(figsize=(12,6)); ax.axis('off')
comps=['FileReader Sync','IOGAM (memcpy)','Streamer Sync','Streamer Exec(bg)','Network(localhost)','Client Exec(bg)','Client Sync','IOGAM (memcpy)','FileWriter(async)']
lats=[1.0,0.1,1.0,1.0,0.05,1.0,0.01,0.1,50.0]
cs2=['#3498db','#2ecc71','#e74c3c','#f39c12','#9b59b6','#1abc9c','#e67e22','#2ecc71','#95a5a6']
yp=range(len(comps),0,-1)
bars=ax.barh(list(yp),lats,color=cs2,edgecolor='white',lw=1.5)
for b,l in zip(bars,lats):
ax.text(b.get_width()+0.2,b.get_y()+b.get_height()/2,f'{l:.1f} ms' if l>=1 else f'{l*1000:.0f} µs',va='center',fontsize=9,fontweight='bold')
ax.text(0.2,b.get_y()+b.get_height()/2,comps[len(comps)-int(b.get_y()+b.get_height())],va='center',fontsize=8,color='white',fontweight='bold')
ax.set_xlabel('Latency (ms)',fontsize=11)
ax.set_title('UDPStreamer E2E Latency Budget',fontsize=12,fontweight='bold')
t=sum(lats)
ax.text(0.15,-0.4,f'Total: {t:.1f} ms | Max throughput: {1000/t:.0f} Hz',fontsize=11,fontweight='bold',transform=ax.get_xaxis_transform())
plt.tight_layout(); plt.savefig('latency_budget.png',dpi=150); plt.close()
print(' ✓ latency_budget.png')
print(' All plots generated.')
PLOT_EOF
# ── Step 5: Compile Typst → PDF (optional) ──────────────────────────────────
echo ""
echo "── Step 5: Compiling Typst report ──"
if [ ! -f "${SCRIPT_DIR}/E2E_Report.typ" ]; then
echo " SKIP: E2E_Report.typ template not present."
elif ! command -v typst >/dev/null 2>&1; then
echo " SKIP: typst not installed."
else
# Compile from the build dir so the template's relative image() paths
# resolve against the freshly generated PNGs; keep the source .typ pristine.
cp "${SCRIPT_DIR}/E2E_Report.typ" "${OUT_DIR}/E2E_Report.typ"
typst compile "${OUT_DIR}/E2E_Report.typ" "${OUT_DIR}/E2E_Report.pdf" 2>&1
if [ -f "${OUT_DIR}/E2E_Report.pdf" ]; then
SIZE=$(ls -lh "${OUT_DIR}/E2E_Report.pdf" | awk '{print $5}')
echo " ✓ Report generated: ${OUT_DIR}/E2E_Report.pdf (${SIZE})"
else
echo " ✗ Typst compilation failed"
fi
fi
echo ""
echo "=========================================="
echo " Done — artifacts in ${OUT_DIR}"
echo "=========================================="
+174
View File
@@ -0,0 +1,174 @@
#!/usr/bin/env python3
"""
validate_binary.py — Compare MARTe2 binary input and output files.
Usage: python3 validate_binary.py <input.bin> <output.bin> [--label NAME]
Reads MARTe2 binary format (header: 4B numSigs, per-sig: 2B type + 32B name + 4B elems),
then compares signal count, element counts, and data rows between input and output.
Output rows are matched against input rows via set membership to handle
async FileWriter flush ordering.
"""
import struct, sys, os
def read_binary(path):
"""Read MARTe2 binary file. Returns (signals, raw_data_bytes)."""
if not os.path.exists(path):
return None, None
with open(path, 'rb') as f:
data = f.read()
if len(data) < 4:
return None, None
ns = struct.unpack_from('<I', data, 0)[0]
sigs = []
off = 4
for _ in range(ns):
if off + 38 > len(data):
break
tc = struct.unpack_from('<H', data, off)[0]
nm = data[off+2:off+34].rstrip(b'\x00').decode(errors='replace')
ne = struct.unpack_from('<I', data, off+34)[0]
sigs.append((nm, tc, ne))
off += 38
raw = data[off:]
return sigs, raw
def validate(input_path, output_path, label="test"):
"""Validate output against input. Returns (passed, message, details)."""
in_sigs, in_raw = read_binary(input_path)
out_sigs, out_raw = read_binary(output_path)
if in_sigs is None:
return False, f"[{label}] Cannot read input file: {input_path}", {}
if out_sigs is None or out_raw is None:
return False, f"[{label}] Cannot read output file: {output_path}", {}
# Signal validation
if len(in_sigs) != len(out_sigs):
return False, f"[{label}] Signal count mismatch: {len(in_sigs)} in vs {len(out_sigs)} out", {}
for i, (si, so) in enumerate(zip(in_sigs, out_sigs)):
if si[2] != so[2]:
return False, f"[{label}] Signal '{si[0]}' size mismatch: {si[2]} in vs {so[2]} out", {}
total_elems = sum(ne for _, _, ne in in_sigs)
in_row_bytes = total_elems * 4
out_row_bytes = sum(ne for _, _, ne in out_sigs) * 4
if in_row_bytes != out_row_bytes:
return False, f"[{label}] Row size mismatch: {in_row_bytes} in vs {out_row_bytes} out", {}
n_rows_in = len(in_raw) // in_row_bytes if in_row_bytes > 0 else 0
n_rows_out = len(out_raw) // out_row_bytes if out_row_bytes > 0 else 0
if n_rows_out == 0:
return False, f"[{label}] Output file has no data rows", {}
# Build input row set
input_rows = set()
for r in range(n_rows_in):
input_rows.add(in_raw[r * in_row_bytes:(r + 1) * in_row_bytes])
# Classify each output row into exactly one of three buckets:
# zero — all-zero row (benign startup transient before first DATA)
# matching — non-zero row that equals some input row (correct transport)
# mismatching — non-zero row that matches no input row (data corruption)
zero_rows = 0
matching = 0
mismatching = 0
first_zero = -1
first_match = -1
first_mismatch = -1
for r in range(n_rows_out):
row = out_raw[r * out_row_bytes:(r + 1) * out_row_bytes]
if not any(row):
zero_rows += 1
if first_zero < 0:
first_zero = r
elif row in input_rows:
matching += 1
if first_match < 0:
first_match = r
else:
mismatching += 1
if first_mismatch < 0:
first_mismatch = r
details = {
"n_signals": len(in_sigs),
"signal_names": [s[0] for s in in_sigs],
"signal_sizes": [s[2] for s in in_sigs],
"row_bytes": in_row_bytes,
"n_rows_in": n_rows_in,
"n_rows_out": n_rows_out,
"zero_rows": zero_rows,
"matching_rows": matching,
"mismatching_rows": mismatching,
"first_zero_row": first_zero,
"first_matching_row": first_match,
"first_mismatch_row": first_mismatch,
"input_bytes": len(in_raw),
"output_bytes": len(out_raw),
}
# Fail threshold:
# - any non-zero output row that matches no input row → data corruption → FAIL
# - no matching rows at all (incl. all-zero output) → FAIL
if matching == 0:
return False, (f"[{label}] FAIL: no output row matches any input row "
f"({n_rows_out} rows: {zero_rows} zero, {mismatching} corrupted)"), details
if mismatching > 0:
return False, (f"[{label}] FAIL: {mismatching} non-zero output rows match no input row "
f"(first at row {first_mismatch})"), details
if zero_rows > 0:
return True, (f"[{label}] WARN: {matching}/{n_rows_out} rows match "
f"({zero_rows} startup zero rows; first match at row {first_match})"), details
return True, f"[{label}] PASS: all {n_rows_out} rows match input", details
def main():
import argparse
p = argparse.ArgumentParser(description="Validate MARTe2 binary output against input")
p.add_argument("input", help="Input binary file")
p.add_argument("output", help="Output binary file")
p.add_argument("--label", default="e2e", help="Test label for output messages")
p.add_argument("--json", default=None, help="Write the metrics (incl. pass/fail) to this JSON file")
args = p.parse_args()
passed, msg, details = validate(args.input, args.output, args.label)
if args.json is not None:
import json
record = dict(details)
record["label"] = args.label
record["passed"] = passed
record["status"] = "PASS" if passed else "FAIL"
record["message"] = msg.strip()
with open(args.json, "w") as jf:
json.dump(record, jf, indent=2)
# Always print details
if details:
print(f" Signals: {details['n_signals']} ({', '.join(f'{n}({s})' for n,s in zip(details['signal_names'], details['signal_sizes']))})")
print(f" Row size: {details['row_bytes']} B")
print(f" Input: {details['input_bytes']} B ({details['n_rows_in']} rows)")
print(f" Output: {details['output_bytes']} B ({details['n_rows_out']} rows)")
print(f" Matching: {details['matching_rows']}/{details['n_rows_out']}")
print(f" Zero rows: {details['zero_rows']}/{details['n_rows_out']}")
print(f" Mismatching: {details['mismatching_rows']}/{details['n_rows_out']} (non-zero, unmatched)")
if details['first_matching_row'] >= 0:
print(f" First matching row: {details['first_matching_row']}")
if details['first_zero_row'] >= 0:
print(f" First zero row: {details['first_zero_row']}")
if details['first_mismatch_row'] >= 0:
print(f" First mismatching row: {details['first_mismatch_row']}")
print(f" {'' if passed else ''} {msg}")
sys.exit(0 if passed else 1)
if __name__ == "__main__":
main()
@@ -0,0 +1,188 @@
# StreamHub Binary Recorder — Design
**Date:** 2026-06-25
**Component:** `Source/Applications/StreamHub/`
**Status:** Approved (design phase)
## Goal
Add an option to the headless C++ StreamHub app to store incoming signal data to
disk in MARTe2 FileWriter-compatible binary format. The operator can record **all
signals** ("full packets") or a **user-specified subset**. Capture is **lossless**
(full source rate) and produces files **byte-identical to a MARTe2 FileWriter
capture**, so they validate against the existing `Test/E2E/.../validate_binary.py`
tooling.
## Locked Decisions
| Decision | Choice |
|---|---|
| Tap point | Packet-decode time (lossless, full source rate) |
| Data types | Native packet types (byte-identical to FileWriter) |
| File layout | One `.bin` file per source/session |
| Retention | Rotating files: size cap (`MaxFileMB`) + keep newest `KeepFiles` |
| Control | Auto-start on launch (config) + WS `recStart`/`recStop` |
| Signal subset | Config default (`"all"` or list) + WS override per run |
| Disk-write threading | **Approach C**: receive thread serializes into a per-source double-buffer; the existing 30 Hz push thread flushes to disk |
## Architecture
### Component & ownership
New `BinaryRecorder` class (`BinaryRecorder.{h,cpp}`), **one instance owned by each
`UDPSourceSession`** — the per-source analog of the global `HistoryWriter`.
```
UDPSourceSession
├─ rings_[] (float64, live/zoom/trigger) ← unchanged
├─ stats_ ← unchanged
└─ recorder_ (BinaryRecorder) ← NEW: native-type, lossless, file-per-source
```
- The session configures its recorder when it knows its signal layout (first CONFIG,
in `ParseConfigPayload`), since the on-disk header is derived from the signal
descriptors and the resolved subset.
- Lifecycle mirrors the session: `Configure(descs, subset) → [recording] → Stop()`.
A CONFIG change closes the current file and re-opens with a new header.
- The recorder object always exists; it only writes when **armed** (auto-armed at
startup if config enables it; toggled by WS).
### On-disk format (byte-identical to MARTe2 FileWriter)
```
[u32 numSigs]
per signal: [u16 typeCode][char[32] name][u32 numElements] (38 B)
then rows: each row = one cycle, all subset signals, signal-major,
native types, little-endian
```
- `numSigs`, descriptors, and `numElements` come from the resolved subset, so a
subset capture is a valid standalone FileWriter file containing only the chosen
signals.
- **Native-type reconstruction:** the wire payload may be quantized
(`quantType != NONE`). The recorder dequantizes to the physical value then
re-encodes to the descriptor's `typeCode` (float32/uint32/int16/...). For
`quantType == NONE` it is a straight byte copy.
### Rotation (size cap + keep N)
- Files named `<sourceId>_<UTC-timestamp>.bin` in the configured directory.
- When the active file reaches `MaxFileMB`: close it, open a new one (fresh header),
delete the oldest until at most `KeepFiles` remain for that source.
- `MinDiskFreeMB` guard (like HistoryWriter): if free space drops below it, stop
writing (disarm) and log, rather than filling the disk.
### ACCUMULATE publish mode
A packet can carry `numSamples` values for an accumulated scalar while other signals
carry 1. To keep fixed-width FileWriter rows, the recorder emits **`numSamples` rows
per packet**, repeating the non-accumulated signals' values across those rows (the
natural "one row per cycle" expansion, matching how the rings already receive
`numSamples` writes).
### Capture path (receive thread)
In `ParseDataPayload`, after the existing decode/ring-write loop, if the recorder is
armed:
- Serialize the packet into native-type FileWriter rows (1 row, or `numSamples` rows
in ACCUMULATE) and append the bytes into the **active staging buffer**.
- Staging is a per-source **double buffer** (two growable byte buffers,
`front`/`back`). The receive thread always appends to `front`. A short
`FastPollingMutexSem` protects only the buffer-swap and append bookkeeping — never
the disk.
- **Overflow guard:** each staging buffer has a soft cap (`StagingMB`, default 8 MB).
If the push thread has fallen so far behind that `front` would exceed the cap, the
recorder increments a `droppedRows` counter and skips the append (keeping reception
alive). Reported via stats/WS so loss is observable, not silent.
- No disk syscalls ever run on the receive thread.
### Flush path (push thread)
Inside the existing 30 Hz push loop, after rings/zoom for each session, the recorder
does its disk work on the **push thread**:
1. **Swap** `front`/`back` under the short mutex (receive thread keeps appending to
the new `front` immediately).
2. **Write** the filled `back` buffer with `pwrite()` at the running offset; clear
`back` for reuse.
3. **Rotate** if the file offset crossed `MaxFileMB` (close, open new with header,
prune to `KeepFiles`, check `MinDiskFreeMB`).
4. **fsync cadence:** `fdatasync()` every `FlushIntervalSec` (like HistoryWriter),
not every tick.
Arm/disarm/rotate requests from WS (other threads) are applied here via a small
pending-flag, so all file open/close/unlink happens on one thread — no cross-thread
file races. Durability latency is bounded to one push tick (~33 ms) plus the fsync
interval.
## Configuration
New `+Recorder` block in the StreamHub cfg (parsed in `Initialise`, try
`+Recorder`/`Recorder`):
```
+Recorder = {
Enabled = 1 // master enable; arms at startup if AutoStart=1
AutoStart = 1 // begin recording on launch
Directory = "/var/streamhub/rec" // required
Signals = "all" // "all" OR comma-separated "src:sig" keys
MaxFileMB = 256 // size cap → roll
KeepFiles = 8 // newest N kept per source
StagingMB = 8 // per-source staging soft cap (overflow guard)
FlushIntervalSec= 5 // fdatasync cadence
MinDiskFreeMB = 500 // stop-writing guard
}
```
- `Signals = "all"` → every signal of every source. A subset list selects per-source
signals by `src:sig` key; a source with no selected signals does not record.
## WebSocket control
Dispatched in `OnWSCommand` via the existing strcmp-on-`type` pattern; replies
unicast.
- `recStart``{type:"recStart", signals?:"all"|["src:sig",...]}` → arm; optional
`signals` overrides the config subset for this run.
- `recStop` — disarm, flush, close files.
- `recInfo` — returns
`{enabled, recording, perSource:[{id, file, bytesWritten, rowsWritten, droppedRows, freeMB}]}`.
- `recStatus` event broadcast on arm/disarm/rotate/overflow so clients reflect state
without polling.
## Testing
### Unit tests (GTest, `Test/Applications/StreamHub/`)
- **Header/format:** configure with a known descriptor set + subset → assert written
header bytes match the FileWriter layout (`numSigs`, 38-byte descriptors, native
`typeCode`/`numElements`).
- **Native-type re-encode:** feed quantized and non-quantized inputs → assert on-disk
bytes equal the expected native-type encoding.
- **ACCUMULATE expansion:** packet with `numSamples>1` for a scalar → assert
`numSamples` rows emitted, non-accumulated signals repeated.
- **Rotation:** drive past `MaxFileMB` → assert roll, new header, prune to
`KeepFiles`, oldest deleted.
- **Overflow guard:** stall the flush, overfill staging → assert `droppedRows`
increments and the reception path does not block.
- **Subset selection:** `"all"` vs explicit `src:sig` list → assert only chosen
signals appear in the file.
### E2E (extend `Test/E2E/`)
- Add a recorder scenario: run StreamHub with `+Recorder` against a live UDPStreamer
source, then validate the produced `.bin` with the existing `validate_binary.py`
(checks signal count/sizes + row matching).
- Reuse `run_e2e_report.sh`'s output-dir + JSON + Typst-report plumbing so the
recorder result lands in the report alongside the unicast/multicast results.
## Risks / Notes
- **Native re-encode vs quantization:** correctness depends on the dequantize +
re-encode path matching the original type exactly; covered by a dedicated unit test.
- **Staging memory:** `StagingMB` per source bounds RAM; overflow is observable via
`droppedRows` rather than unbounded growth.
- **Rotation hiccups:** file open/unlink on the push thread (non-RT), never on the
receive path.
- **CONFIG mid-recording:** re-headers a new file; old file is closed cleanly.
+3 -2
View File
@@ -98,8 +98,9 @@ if [[ "$SKIP_BUILD" -eq 0 ]]; then
if [[ "$START_GUI" -eq 1 ]]; then if [[ "$START_GUI" -eq 1 ]]; then
if [[ -d "${SCRIPT_DIR}/Client/streamhub/build" ]]; then if [[ -d "${SCRIPT_DIR}/Client/streamhub/build" ]]; then
echo "==> Building ImGui client..." echo "==> Building ImGui client (a full rebuild can take ~2 min;"
cmake --build "${SCRIPT_DIR}/Client/streamhub/build" -j"$(nproc)" 2>&1 | tail -5 echo " ImPlot's implot_items.cpp is one slow -O3 translation unit)..."
cmake --build "${SCRIPT_DIR}/Client/streamhub/build" -j"$(nproc)"
fi fi
fi fi