Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0412c20edd | |||
| dca4872976 |
+318
-111
@@ -75,9 +75,21 @@ void App::update() {
|
||||
if (nowConnected && !prevConnected_) {
|
||||
ws_.sendText(BuildGetSources());
|
||||
ws_.sendText(BuildGetStats());
|
||||
ws_.sendText(BuildHistoryInfo());
|
||||
}
|
||||
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 */
|
||||
ws_.poll([this](const WSMessage& msg) {
|
||||
if (msg.isBinary) {
|
||||
@@ -99,8 +111,7 @@ void App::update() {
|
||||
| ImGuiWindowFlags_NoResize
|
||||
| ImGuiWindowFlags_NoMove
|
||||
| ImGuiWindowFlags_NoBringToFrontOnFocus
|
||||
| ImGuiWindowFlags_NoNavFocus
|
||||
| ImGuiWindowFlags_MenuBar;
|
||||
| ImGuiWindowFlags_NoNavFocus;
|
||||
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
|
||||
@@ -108,8 +119,8 @@ void App::update() {
|
||||
ImGui::Begin("##main", nullptr, wf);
|
||||
ImGui::PopStyleVar(3);
|
||||
|
||||
renderMenuBar();
|
||||
renderToolbar();
|
||||
if (showHistBar_) { renderHistoryBar(); }
|
||||
if (showTrigBar_) { renderTriggerBar(); }
|
||||
|
||||
/* Horizontal split: sidebar | plot grid */
|
||||
@@ -136,90 +147,33 @@ void App::update() {
|
||||
if (showAddSrc_) { renderAddSourceModal(); }
|
||||
}
|
||||
|
||||
/* ── Menu bar ────────────────────────────────────────────────────────────── */
|
||||
|
||||
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();
|
||||
}
|
||||
/* renderMenuBar() removed — all functionality merged into toolbar + sidebar */
|
||||
|
||||
/* ── Toolbar ─────────────────────────────────────────────────────────────── */
|
||||
|
||||
void App::renderToolbar() {
|
||||
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 kIconRows[] = {1,1,2,1,3,2,1,4};
|
||||
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");
|
||||
}
|
||||
if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Layout"); }
|
||||
ImGui::SameLine();
|
||||
if (ImGui::BeginPopup("##layout_pop")) {
|
||||
ImGui::TextDisabled("Select layout");
|
||||
@@ -228,7 +182,6 @@ void App::renderToolbar() {
|
||||
for (int li = 0; li < static_cast<int>(PlotLayout::kCount); li++) {
|
||||
bool sel = (static_cast<int>(layout_) == li);
|
||||
ImVec2 p = ImGui::GetCursorScreenPos();
|
||||
/* Invisible button sized to the icon */
|
||||
if (ImGui::InvisibleButton(PlotLayoutNames[li], kIconSz)) {
|
||||
setLayout(static_cast<PlotLayout>(li));
|
||||
ImGui::CloseCurrentPopup();
|
||||
@@ -242,7 +195,7 @@ void App::renderToolbar() {
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
|
||||
/* Global pause */
|
||||
/* ── Global pause ────────────────────────────────────────────────────── */
|
||||
if (ImGui::Button(globalPaused_ ? ICON_FA_PLAY " Resume"
|
||||
: ICON_FA_PAUSE " Pause")) {
|
||||
globalPaused_ = !globalPaused_;
|
||||
@@ -253,45 +206,77 @@ void App::renderToolbar() {
|
||||
}
|
||||
ImGui::SameLine();
|
||||
|
||||
/* Cursors A/B (global: shared & synchronised across all plots) */
|
||||
/* ── Cursors A/B toggle ──────────────────────────────────────────────── */
|
||||
{
|
||||
if (cursorsOn_) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.537f,0.706f,0.980f,0.4f));
|
||||
}
|
||||
if (ImGui::Button(ICON_FA_CROSSHAIRS " Cursors")) {
|
||||
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 (ImGui::Button(ICON_FA_CROSSHAIRS "##curs")) { cursorsOn_ = !cursorsOn_; }
|
||||
if (cursorsOn_) { ImGui::PopStyleColor(); }
|
||||
if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Cursors A/B"); }
|
||||
}
|
||||
ImGui::SameLine();
|
||||
|
||||
/* Trigger bar toggle */
|
||||
if (ImGui::Button(ICON_FA_BOLT " Trigger")) { showTrigBar_ = !showTrigBar_; }
|
||||
/* ── Trigger bar toggle ──────────────────────────────────────────────── */
|
||||
{
|
||||
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();
|
||||
|
||||
/* Stats */
|
||||
if (ImGui::Button(ICON_FA_CHART_COLUMN " Stats")) { showStats_ = !showStats_; }
|
||||
/* ── History browse ──────────────────────────────────────────────────── */
|
||||
{
|
||||
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();
|
||||
|
||||
/* Add source */
|
||||
if (ImGui::Button(ICON_FA_PLUS " Source")) { showAddSrc_ = true; }
|
||||
ImGui::SameLine();
|
||||
|
||||
/* Time window presets (Ctrl+scroll on a plot still adjusts freely) */
|
||||
/* ── Time window presets ─────────────────────────────────────────────── */
|
||||
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"};
|
||||
char winPreview[16];
|
||||
snprintf(winPreview, sizeof(winPreview), "%.3g s", windowSec_);
|
||||
ImGui::SetNextItemWidth(70.f);
|
||||
if (ImGui::BeginCombo("Win", winPreview)) {
|
||||
if (ImGui::BeginCombo("##win", winPreview)) {
|
||||
for (int wi = 0; wi < 5; wi++) {
|
||||
bool sel = std::fabs(windowSec_ - kWinPresets[wi]) < 1e-9;
|
||||
if (ImGui::Selectable(kWinLabels[wi], sel)) {
|
||||
@@ -301,18 +286,213 @@ void App::renderToolbar() {
|
||||
}
|
||||
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();
|
||||
|
||||
/* Max points control */
|
||||
ImGui::SetNextItemWidth(80.f);
|
||||
int mp = static_cast<int>(maxPoints_);
|
||||
if (ImGui::InputInt("MaxPts", &mp, 0, 0,
|
||||
ImGuiInputTextFlags_EnterReturnsTrue)) {
|
||||
if (mp >= 2) {
|
||||
maxPoints_ = static_cast<uint32_t>(mp);
|
||||
ws_.sendText(BuildSetMaxPoints(maxPoints_));
|
||||
/* ── Live button ── */
|
||||
if (ImGui::SmallButton(ICON_FA_TOWER_BROADCAST " Live")) {
|
||||
showHistBar_ = false;
|
||||
for (int i = 0; i < numPlotSlots(); i++) {
|
||||
liveFollow_[i] = true;
|
||||
}
|
||||
ImGui::PopStyleVar();
|
||||
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::Separator();
|
||||
@@ -521,6 +701,7 @@ void App::handleBinary(const uint8_t* data, size_t len) {
|
||||
trigger_.status = "triggered";
|
||||
trigger_.trigTime = capture_.trigTime;
|
||||
trigger_.hasTrigTime = true;
|
||||
resetTrigZoom();
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -660,10 +841,24 @@ void App::onZoom(const std::string& json) {
|
||||
for (int i = 0; i < kMaxPlotSlots; i++) {
|
||||
auto& zc = zoomCache_[i];
|
||||
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.t0 = zc.reqT0;
|
||||
zc.t1 = zc.reqT1;
|
||||
zc.t0 = dataT0;
|
||||
zc.t1 = dataT1;
|
||||
zc.valid = true;
|
||||
}
|
||||
zc.pending = false;
|
||||
return;
|
||||
}
|
||||
@@ -717,10 +912,22 @@ void App::onHistoryZoom(const std::string& json) {
|
||||
for (int i = 0; i < kMaxPlotSlots; i++) {
|
||||
auto& hc = histZoomCache_[i];
|
||||
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.t0 = hc.reqT0;
|
||||
hc.t1 = hc.reqT1;
|
||||
hc.t0 = dataT0;
|
||||
hc.t1 = dataT1;
|
||||
hc.valid = true;
|
||||
}
|
||||
hc.pending = false;
|
||||
return;
|
||||
}
|
||||
|
||||
+13
-3
@@ -26,7 +26,7 @@ namespace StreamHubClient {
|
||||
/** One signal as known to the client. */
|
||||
struct Signal {
|
||||
SignalMeta meta;
|
||||
SignalBuffer buf{20000};
|
||||
SignalBuffer buf{1000000};
|
||||
ImVec4 color{0.4f, 0.8f, 1.0f, 1.0f};
|
||||
float lineWidth = 1.5f;
|
||||
int marker = -1; /* ImPlotMarker (-1 = none) */
|
||||
@@ -244,10 +244,15 @@ public:
|
||||
/** @brief Hi-res history zoom cache (per plot, parallel to zoomCache_). */
|
||||
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. */
|
||||
bool& showStats() { return showStats_; }
|
||||
bool& showAddSrc() { return showAddSrc_; }
|
||||
bool& showTrigBar() { return showTrigBar_; }
|
||||
bool& showHistBar() { return showHistBar_; }
|
||||
|
||||
/** @brief Find source index by id (-1 if not found). */
|
||||
int findSource(const std::string& id) const;
|
||||
@@ -270,8 +275,8 @@ private:
|
||||
void onMaxPointsUpdated(const std::string& json);
|
||||
|
||||
/* ---- Rendering ------------------------------------------------------ */
|
||||
void renderMenuBar();
|
||||
void renderToolbar();
|
||||
void renderHistoryBar();
|
||||
void renderTriggerBar();
|
||||
void renderSidebar();
|
||||
void renderPlotGrid();
|
||||
@@ -285,7 +290,7 @@ private:
|
||||
|
||||
TriggerState trigger_;
|
||||
PlotLayout layout_ = PlotLayout::kLayout1x1;
|
||||
uint32_t maxPoints_ = 20000u;
|
||||
uint32_t maxPoints_ = 1000000u;
|
||||
bool globalPaused_ = false;
|
||||
|
||||
/* Plot slots: one vector of assignments per plot panel */
|
||||
@@ -304,6 +309,9 @@ private:
|
||||
double cursorA_ = 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) */
|
||||
PlotSnapshot plotSnap_[kMaxPlotSlots];
|
||||
|
||||
@@ -315,11 +323,13 @@ private:
|
||||
|
||||
/* History info from the hub */
|
||||
HistoryInfoMsg historyInfo_;
|
||||
double lastHistInfoReq_ = 0.0;
|
||||
|
||||
/* UI state */
|
||||
bool showStats_ = false;
|
||||
bool showAddSrc_ = false;
|
||||
bool showTrigBar_ = false;
|
||||
bool showHistBar_ = false;
|
||||
bool sidebarOpen_ = true;
|
||||
|
||||
/* Connection fields (populated by init(), editable in menu bar) */
|
||||
|
||||
+119
-44
@@ -39,8 +39,10 @@ struct DragPayload { int srcIdx; int sigIdx; };
|
||||
static const int kPlotSlotsMax = 8; /* mirrors App::kMaxPlotSlots */
|
||||
|
||||
/* Live windows at or below this width refresh via hub hi-res zoom fetches
|
||||
* instead of (in addition to) the decimated push stream. */
|
||||
static const double kLiveHiResMaxWin = 2.0; /* seconds */
|
||||
* instead of (in addition to) the decimated push stream. Wide windows
|
||||
* 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 ─────────────────────────────────────────────────────────────── */
|
||||
|
||||
@@ -198,18 +200,30 @@ void RenderPlotPanel(App& app, int plotIdx, bool& paused) {
|
||||
(zc.t1 - zc.t0) >= app.windowSec() * 0.9 &&
|
||||
(wallNow - zc.t1) < 3.0;
|
||||
|
||||
const bool useZoomData = !trigView && zc.valid &&
|
||||
const bool useZoomData = !trigView && !paused && zc.valid &&
|
||||
(liveHiRes ||
|
||||
(!live &&
|
||||
zc.t0 <= app.plotXMin(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);
|
||||
const bool useHistData = !trigView && !live && !useZoomData &&
|
||||
hc.valid &&
|
||||
const bool haveHistCover = hc.valid &&
|
||||
hc.t0 <= app.plotXMin(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 */
|
||||
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);
|
||||
}
|
||||
|
||||
/* Back / Fit (zoom history) — only meaningful when not live */
|
||||
if (!live && !trigView) {
|
||||
/* Back / Fit / Reset (zoom history) */
|
||||
if (!live || (trigView && app.trigZoomed(plotIdx))) {
|
||||
ImGui::SameLine();
|
||||
auto& hist = app.zoomHist(plotIdx);
|
||||
if (hist.empty()) { ImGui::BeginDisabled(); }
|
||||
@@ -380,6 +394,13 @@ void RenderPlotPanel(App& app, int plotIdx, bool& paused) {
|
||||
}
|
||||
if (hist.empty()) { ImGui::EndDisabled(); }
|
||||
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")) {
|
||||
double mn = 1e300, mx = -1e300;
|
||||
for (size_t si = 0; si < slots.size(); si++) {
|
||||
@@ -393,6 +414,7 @@ void RenderPlotPanel(App& app, int plotIdx, bool& paused) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* History indicator */
|
||||
if (useHistData) {
|
||||
@@ -401,13 +423,23 @@ void RenderPlotPanel(App& app, int plotIdx, bool& paused) {
|
||||
ICON_FA_CLOCK_ROTATE_LEFT " HIST");
|
||||
}
|
||||
|
||||
/* Norm mode + cursors toggles */
|
||||
/* Norm/Dig/Mix mode — compact toggle buttons matching SmallButton height */
|
||||
ImGui::SameLine();
|
||||
{
|
||||
static const char* kVModes[] = {"Norm", "Dig", "Mix"};
|
||||
ImGui::SetNextItemWidth(58.f);
|
||||
char vmId[16]; snprintf(vmId, sizeof(vmId), "##vm%d", plotIdx);
|
||||
ImGui::Combo(vmId, &vMode, kVModes, 3);
|
||||
static const char* kVLabels[] = {"N", "D", "M"};
|
||||
static const char* kVTooltips[] = {"Normal", "Digital", "Mixed"};
|
||||
for (int vm = 0; vm < 3; vm++) {
|
||||
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) ───────── */
|
||||
@@ -500,10 +532,17 @@ void RenderPlotPanel(App& app, int plotIdx, bool& paused) {
|
||||
/* Y axis always ±4 div space */
|
||||
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;
|
||||
bool& trigZm = app.trigZoomed(plotIdx);
|
||||
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) {
|
||||
if (liveHiRes) {
|
||||
/* 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 */
|
||||
static double lastHistPush[kPlotSlotsMax] = {};
|
||||
|
||||
if (overPlot && !trigView) {
|
||||
if (overPlot) {
|
||||
ImGuiIO& io2 = ImGui::GetIO();
|
||||
const float wheel = io2.MouseWheel;
|
||||
const bool ctrl = io2.KeyCtrl;
|
||||
const bool shift = io2.KeyShift;
|
||||
const double now = ImGui::GetTime();
|
||||
|
||||
if (wheel != 0.f) {
|
||||
/* Zoom factor: scroll up = zoom in (×0.8), down = zoom out (×1.25) */
|
||||
const double zoomIn = 0.8;
|
||||
const double zoomOut = 1.25;
|
||||
double factor = (wheel > 0.f) ? zoomIn : zoomOut;
|
||||
/* Helper: enter zoomed mode for trigger view (seed from capture window) */
|
||||
auto enterTrigZoom = [&]() {
|
||||
if (trigView && !trigZm) {
|
||||
app.setPlotX(plotIdx, -cap->preSec, cap->postSec);
|
||||
trigZm = true;
|
||||
}
|
||||
};
|
||||
|
||||
if (ctrl) {
|
||||
/* ── X zoom ─────────────────────────────────────────── */
|
||||
if (live) {
|
||||
app.setWindowSec(app.windowSec() * factor);
|
||||
} else {
|
||||
/* Helper: X-zoom the stored range by factor around center */
|
||||
auto xZoomStored = [&](double factor) {
|
||||
if (trigView) { enterTrigZoom(); }
|
||||
if (now - lastHistPush[plotIdx] > 0.6) {
|
||||
app.pushZoomHist(plotIdx);
|
||||
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 half = (app.plotXMax(plotIdx) - app.plotXMin(plotIdx)) * 0.5 * factor;
|
||||
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) {
|
||||
/* ── Y offset of active signal ───────────────────────── */
|
||||
if (actSlot >= 0 && actSlot < (int)slots.size()) {
|
||||
auto& a = slots[actSlot];
|
||||
/* Switch to manual so position is retained */
|
||||
if (a.vs.mode != 2) {
|
||||
a.vs.divValue = std::max(a.vs.resolvedDiv, 1e-30);
|
||||
a.vs.offset = a.vs.resolvedOffset;
|
||||
a.vs.mode = 2;
|
||||
}
|
||||
/* Each scroll step moves by 0.5 screen divisions */
|
||||
a.vs.screenPos += (wheel > 0.f) ? 0.5 : -0.5;
|
||||
}
|
||||
} else {
|
||||
@@ -624,27 +674,21 @@ void RenderPlotPanel(App& app, int plotIdx, bool& paused) {
|
||||
}
|
||||
a.vs.divValue = std::max(a.vs.divValue * factor, 1e-30);
|
||||
} else {
|
||||
/* No active signal: plain scroll falls back to X zoom
|
||||
* so the wheel always does something useful. */
|
||||
if (live) {
|
||||
/* No active signal: plain scroll → X zoom */
|
||||
if (!trigView && live) {
|
||||
app.setWindowSec(app.windowSec() * factor);
|
||||
} else {
|
||||
if (now - lastHistPush[plotIdx] > 0.6) {
|
||||
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);
|
||||
xZoomStored(factor);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 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 (live) {
|
||||
/* Seed stored range from current live window */
|
||||
if (trigView) { enterTrigZoom(); }
|
||||
if (!trigView && live) {
|
||||
app.initPlotX(plotIdx, wallNow);
|
||||
live = false;
|
||||
lastHistPush[plotIdx] = now;
|
||||
@@ -662,8 +706,8 @@ void RenderPlotPanel(App& app, int plotIdx, bool& paused) {
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Hi-res WS zoom requests ────────────────────────────────────── */
|
||||
if (!trigView) {
|
||||
/* ── Hi-res WS zoom requests (suppressed while paused) ──────────── */
|
||||
if (!trigView && !paused) {
|
||||
std::string csv;
|
||||
for (const auto& a : slots) {
|
||||
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 ──────────────────────────────────────────── */
|
||||
static const size_t kMaxPush = 2400;
|
||||
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);
|
||||
}
|
||||
|
||||
/* 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()) {
|
||||
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 kCursB{0.796f,0.651f,0.969f,0.9f};
|
||||
ImPlot::DragLineX(910, &app.cursorA(), kCursA, 1.2f);
|
||||
|
||||
@@ -553,9 +553,8 @@ bool ParseHistoryInfo(const std::string& json, HistoryInfoMsg& out) {
|
||||
out.signals.clear();
|
||||
out.enabled = false;
|
||||
|
||||
char tmp[16] = "";
|
||||
jsonGetStr(json.c_str(), "enabled", tmp, sizeof(tmp));
|
||||
out.enabled = (strcmp(tmp, "true") == 0);
|
||||
/* "enabled" is a JSON boolean (unquoted true/false), not a string */
|
||||
out.enabled = (strstr(json.c_str(), "\"enabled\":true") != nullptr);
|
||||
|
||||
double df = 0.0;
|
||||
jsonGetDouble(json.c_str(), "durationHours", df);
|
||||
|
||||
@@ -100,6 +100,18 @@ void RenderSourcePanel(App& app) {
|
||||
}
|
||||
|
||||
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 */
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
* The trigger runs in the C++ StreamHub: this panel only edits the config
|
||||
* (signal/edge/threshold/window/pre%/mode), sends setTrigger + arm/disarm/
|
||||
* 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"
|
||||
@@ -22,12 +25,10 @@ namespace StreamHubClient {
|
||||
|
||||
/* 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 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 char* kEdgeLabels[] = {ICON_FA_ARROW_TREND_UP " Rising",
|
||||
ICON_FA_ARROW_TREND_DOWN " Falling",
|
||||
ICON_FA_ARROWS_UP_DOWN " Both"};
|
||||
static const char* kEdgeLabels[] = {"Rising", "Falling", "Both"};
|
||||
static const char* kEdgeWire[] = {"rising", "falling", "both"};
|
||||
|
||||
/* Send the current config to the hub. */
|
||||
@@ -44,20 +45,14 @@ void RenderTriggerPanel(App& app) {
|
||||
auto& trig = app.trigger();
|
||||
auto& sources = app.sources();
|
||||
|
||||
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.15f, 0.15f, 0.20f, 1.f));
|
||||
ImGui::BeginChild("##trigbar", ImVec2(0.f, 90.f), true);
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(6.f, 4.f));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(4.f, 4.f));
|
||||
|
||||
bool cfgChanged = false;
|
||||
|
||||
/* ── Signal selector (full keys "src:sig") ─────────────────────────── *
|
||||
* Vector signals are flattened time-series on the hub: one key per
|
||||
* signal; the trigger engine checks every decoded sample of it. */
|
||||
ImGui::SetNextItemWidth(180.f);
|
||||
if (ImGui::BeginCombo("Signal##trig", trig.signalKey.empty()
|
||||
? "<select>" : trig.signalKey.c_str())) {
|
||||
/* ── Signal selector ──────────────────────────────────────────────── */
|
||||
ImGui::SetNextItemWidth(150.f);
|
||||
if (ImGui::BeginCombo("##trigsig", trig.signalKey.empty()
|
||||
? "<signal>" : trig.signalKey.c_str())) {
|
||||
for (const auto& src : sources) {
|
||||
for (const auto& sig : src.signals) {
|
||||
char key[192];
|
||||
@@ -76,14 +71,14 @@ void RenderTriggerPanel(App& app) {
|
||||
ImGui::SameLine();
|
||||
|
||||
/* ── Edge ─────────────────────────────────────────────────────────── */
|
||||
ImGui::SetNextItemWidth(95.f);
|
||||
if (ImGui::Combo("Edge##trig", &trig.edge, kEdgeLabels, 3)) {
|
||||
ImGui::SetNextItemWidth(70.f);
|
||||
if (ImGui::Combo("##trigedge", &trig.edge, kEdgeLabels, 3)) {
|
||||
cfgChanged = true;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
|
||||
/* ── Threshold ───────────────────────────────────────────────────── */
|
||||
ImGui::SetNextItemWidth(80.f);
|
||||
ImGui::SetNextItemWidth(65.f);
|
||||
if (ImGui::InputDouble("Thr##trig", &trig.threshold, 0.0, 0.0, "%.4g",
|
||||
ImGuiInputTextFlags_EnterReturnsTrue)) {
|
||||
cfgChanged = true;
|
||||
@@ -97,7 +92,7 @@ void RenderTriggerPanel(App& app) {
|
||||
winIdx = w; break;
|
||||
}
|
||||
}
|
||||
ImGui::SetNextItemWidth(85.f);
|
||||
ImGui::SetNextItemWidth(75.f);
|
||||
if (ImGui::Combo("Win##trig", &winIdx, kWinLabels, kNumWins)) {
|
||||
trig.windowSec = kWinVals[winIdx];
|
||||
cfgChanged = true;
|
||||
@@ -105,24 +100,21 @@ void RenderTriggerPanel(App& app) {
|
||||
ImGui::SameLine();
|
||||
|
||||
/* ── Pre-trigger % ───────────────────────────────────────────────── */
|
||||
ImGui::SetNextItemWidth(100.f);
|
||||
ImGui::SetNextItemWidth(70.f);
|
||||
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);
|
||||
}
|
||||
if (ImGui::IsItemDeactivatedAfterEdit()) { cfgChanged = true; }
|
||||
ImGui::SameLine();
|
||||
|
||||
/* ── Mode ────────────────────────────────────────────────────────── */
|
||||
ImGui::TextUnformatted("Mode:");
|
||||
ImGui::SameLine();
|
||||
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();
|
||||
if (ImGui::RadioButton("Single", &singleInt, 1)) { cfgChanged = true; }
|
||||
if (ImGui::RadioButton("1x##trig", &singleInt, 1)) { cfgChanged = true; }
|
||||
trig.single = (singleInt != 0);
|
||||
|
||||
ImGui::NewLine();
|
||||
ImGui::SameLine(0.f, 10.f);
|
||||
|
||||
/* ── Status badge ────────────────────────────────────────────────── */
|
||||
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 { badgeColor = ImVec4(0.6f,0.6f,0.6f,1.f); badgeText = "IDLE"; }
|
||||
ImGui::TextColored(badgeColor, "[%s]", badgeText);
|
||||
if (trig.hasTrigTime &&
|
||||
(trig.status == "collecting" || trig.status == "triggered")) {
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("t=%.6f", trig.trigTime);
|
||||
}
|
||||
ImGui::SameLine();
|
||||
|
||||
/* ── Arm / Disarm / Rearm / Stop ─────────────────────────────────── */
|
||||
/* ── Arm / Disarm / Stop ────────────────────────────────────────── */
|
||||
bool canArm = !trig.signalKey.empty();
|
||||
if (!canArm) { ImGui::BeginDisabled(); }
|
||||
if (ImGui::Button(ICON_FA_BOLT " Arm")) {
|
||||
if (ImGui::SmallButton(ICON_FA_BOLT " Arm")) {
|
||||
sendTrigConfig(app);
|
||||
app.ws().sendText(BuildArm());
|
||||
}
|
||||
if (!canArm) { ImGui::EndDisabled(); }
|
||||
ImGui::SameLine();
|
||||
|
||||
if (ImGui::Button("Disarm")) {
|
||||
if (ImGui::SmallButton("Disarm")) {
|
||||
app.ws().sendText(BuildDisarm());
|
||||
}
|
||||
ImGui::SameLine();
|
||||
|
||||
if (trig.single && trig.status == "triggered") {
|
||||
if (ImGui::Button("Rearm")) {
|
||||
app.ws().sendText(BuildRearm());
|
||||
}
|
||||
ImGui::SameLine();
|
||||
}
|
||||
|
||||
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;
|
||||
app.ws().sendText(BuildTrigStop(trig.stopped));
|
||||
}
|
||||
ImGui::SameLine();
|
||||
}
|
||||
|
||||
/* Trigger time readout (inline) */
|
||||
if (trig.hasTrigTime &&
|
||||
(trig.status == "collecting" || trig.status == "triggered")) {
|
||||
ImGui::TextDisabled("t=%.6f", trig.trigTime);
|
||||
} else {
|
||||
ImGui::NewLine();
|
||||
}
|
||||
|
||||
/* Config edits while armed take effect immediately on the hub */
|
||||
if (cfgChanged && !trig.signalKey.empty()) {
|
||||
@@ -177,7 +163,6 @@ void RenderTriggerPanel(App& app) {
|
||||
}
|
||||
|
||||
ImGui::PopStyleVar();
|
||||
ImGui::EndChild();
|
||||
ImGui::Separator();
|
||||
}
|
||||
|
||||
|
||||
Binary file not shown.
@@ -15,7 +15,9 @@ apps:
|
||||
$(MAKE) -C Source/Applications/StreamHub -f Makefile.gcc
|
||||
|
||||
core:
|
||||
$(MAKE) -C Source/Components/Interfaces/UDPStream -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/TimeArrayGAM -f Makefile.gcc
|
||||
$(MAKE) -C Source/Components/Interfaces/TCPLogger -f Makefile.gcc
|
||||
@@ -29,7 +31,9 @@ test:
|
||||
|
||||
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/UDPStreamerClient -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/Interfaces/TCPLogger -f Makefile.gcc clean
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
#include "HistoryWriter.h"
|
||||
#include "AdvancedErrorManagement.h"
|
||||
#include "UDPSProtocol.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
@@ -129,8 +130,8 @@ void HistoryWriter::OnSourceConfigured(uint32 sessionIdx, const char *sourceId,
|
||||
MARTe::UDPSSignalDescriptor desc;
|
||||
if (!sess.GetSignalDescriptor(s, desc)) { continue; }
|
||||
|
||||
/* Skip time-only signals (type uint64, typically TimeArray) */
|
||||
if (desc.typeCode == 13u) { continue; } /* uint64 = 13 in UDPS */
|
||||
/* Skip time-reference signals (uint64 type, typically TimeArray) */
|
||||
if (desc.typeCode == MARTe::UDPS_TYPECODE_UINT64) { continue; }
|
||||
|
||||
float64 estRate = static_cast<float64>(desc.samplingRate);
|
||||
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++) {
|
||||
if (!fileActive_[i][s]) { continue; }
|
||||
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,
|
||||
"%s\"%s:%s\":{\"t0\":%.17g,\"t1\":%.17g,\"count\":%u,\"capacity\":%u}",
|
||||
(first ? "" : ","),
|
||||
|
||||
@@ -42,6 +42,12 @@ public:
|
||||
*/
|
||||
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.
|
||||
* @return Number of points actually written.
|
||||
@@ -144,6 +150,22 @@ inline void SignalRingBuffer::Write(float64 t, float64 v) {
|
||||
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 {
|
||||
if ((capacity == 0u) || (count == 0u)) { return 0u; }
|
||||
(void) mutex.FastLock();
|
||||
|
||||
@@ -255,13 +255,27 @@ bool StreamHub::Run() {
|
||||
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()) {
|
||||
uint32 histFlushDiv = history_.Decimation() > 0u
|
||||
? pushRateHz_ * 5u : pushRateHz_ * 5u; /* every 5s */
|
||||
if (histFlushDiv == 0u) { histFlushDiv = 1u; }
|
||||
if ((tickCount_ % histFlushDiv) == 0u) {
|
||||
uint32 flushIntervalTicks = pushRateHz_ * 5u; /* default 5 s */
|
||||
if (pushRateHz_ > 0u) {
|
||||
flushIntervalTicks = pushRateHz_ * 5u;
|
||||
}
|
||||
if (flushIntervalTicks == 0u) { flushIntervalTicks = 1u; }
|
||||
if ((tickCount_ % flushIntervalTicks) == 0u) {
|
||||
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);
|
||||
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() {
|
||||
|
||||
@@ -63,7 +63,10 @@ UDPSourceSession::UDPSourceSession()
|
||||
statLastRxTicks_(0u),
|
||||
statFragCount_(0u),
|
||||
statByteCount_(0u),
|
||||
hrtFreq_(static_cast<float64>(MARTe::HighResolutionTimer::Frequency())),
|
||||
timeScratch_(static_cast<float64 *>(0)),
|
||||
valScratch_(static_cast<float64 *>(0)),
|
||||
tsBatchScratch_(static_cast<float64 *>(0)),
|
||||
timeScratchLen_(0u),
|
||||
trigEngine_(static_cast<TriggerEngine *>(0)),
|
||||
trigEpochSeen_(0xFFFFFFFFu),
|
||||
@@ -76,6 +79,8 @@ UDPSourceSession::UDPSourceSession()
|
||||
UDPSourceSession::~UDPSourceSession() {
|
||||
(void) Stop();
|
||||
delete[] timeScratch_;
|
||||
delete[] valScratch_;
|
||||
delete[] tsBatchScratch_;
|
||||
}
|
||||
|
||||
void UDPSourceSession::ResetCalibration() {
|
||||
@@ -84,6 +89,7 @@ void UDPSourceSession::ResetCalibration() {
|
||||
timeSigCalib_[i] = 0.0;
|
||||
lastPktWallValid_[i] = false;
|
||||
lastPktWallS_[i] = 0.0;
|
||||
accScalarPrevN_[i] = 0u;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -227,7 +233,11 @@ void UDPSourceSession::ParseConfigPayload(const uint8 *payload, uint32 size) {
|
||||
}
|
||||
if (maxElems > timeScratchLen_) {
|
||||
delete[] timeScratch_;
|
||||
delete[] valScratch_;
|
||||
delete[] tsBatchScratch_;
|
||||
timeScratch_ = new float64[maxElems];
|
||||
valScratch_ = new float64[maxElems];
|
||||
tsBatchScratch_ = new float64[maxElems];
|
||||
timeScratchLen_ = maxElems;
|
||||
}
|
||||
|
||||
@@ -369,17 +379,19 @@ void UDPSourceSession::ParseDataPayload(const uint8 *payload, uint32 size) {
|
||||
} else {
|
||||
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;
|
||||
DecodeElems(payload, sigOff[s], desc, nElems, valScratch_);
|
||||
for (uint32 e = 0u; e < nElems; e++) {
|
||||
const float64 val = DecodeOneElem(payload, sigOff[s], desc, e);
|
||||
const float64 t = anchorIsFirstSample
|
||||
tsBatchScratch_[e] = anchorIsFirstSample
|
||||
? (anchor + static_cast<float64>(e) * dt)
|
||||
: (anchor - static_cast<float64>(nElems - 1u - e) * dt);
|
||||
WriteSample(s, e, t, val);
|
||||
}
|
||||
WriteSampleBatch(s, tsBatchScratch_, valScratch_, nElems);
|
||||
}
|
||||
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) &&
|
||||
(nElems <= timeScratchLen_)) {
|
||||
DecodeElems(payload, sigOff[tIdx], descs[tIdx], nElems, timeScratch_);
|
||||
@@ -390,15 +402,18 @@ void UDPSourceSession::ParseDataPayload(const uint8 *payload, uint32 size) {
|
||||
timeSigCalibValid_[tIdx] = true;
|
||||
}
|
||||
const float64 calib = timeSigCalib_[tIdx];
|
||||
DecodeElems(payload, sigOff[s], desc, nElems, valScratch_);
|
||||
for (uint32 e = 0u; e < nElems; e++) {
|
||||
const float64 val = DecodeOneElem(payload, sigOff[s], desc, e);
|
||||
WriteSample(s, e, calib + timeScratch_[e] * timerToSec, val);
|
||||
tsBatchScratch_[e] = calib + timeScratch_[e] * timerToSec;
|
||||
}
|
||||
WriteSampleBatch(s, tsBatchScratch_, valScratch_, nElems);
|
||||
} else {
|
||||
/* No time signal: all samples get arrival wall time. */
|
||||
DecodeElems(payload, sigOff[s], desc, nElems, valScratch_);
|
||||
for (uint32 e = 0u; e < nElems; e++) {
|
||||
const float64 val = DecodeOneElem(payload, sigOff[s], desc, e);
|
||||
WriteSample(s, e, wallNowS, val);
|
||||
tsBatchScratch_[e] = wallNowS;
|
||||
}
|
||||
WriteSampleBatch(s, tsBatchScratch_, valScratch_, nElems);
|
||||
}
|
||||
}
|
||||
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);
|
||||
WriteSample(s, 0u, wallNowS, val);
|
||||
}
|
||||
else if (lastPktWallValid_[s] && (wallNowS > lastPktWallS_[s])) {
|
||||
/* Accumulated scalar: the packet carries one sample per RT
|
||||
* cycle — spread them across the inter-packet wall-clock gap
|
||||
* (same scheme as packed arrays) instead of stamping them all
|
||||
* with the arrival time, which renders as a stair plot. */
|
||||
const float64 dt = (wallNowS - lastPktWallS_[s]) /
|
||||
static_cast<float64>(nElems);
|
||||
const float64 base = lastPktWallS_[s];
|
||||
else {
|
||||
/* Accumulated scalar: reconstruct per-sample timestamps from
|
||||
* the packet's embedded sender HRT (the acquisition time of the
|
||||
* oldest sample), NOT the packet arrival time. The kernel
|
||||
* frequently delivers several queued datagrams in one burst, so
|
||||
* two packets can be processed microseconds apart even though
|
||||
* each represents ~10 ms of signal; arrival-time interpolation
|
||||
* 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++) {
|
||||
const float64 val = DecodeOneElem(payload, sigOff[s], desc, e);
|
||||
WriteSample(s, 0u, base + static_cast<float64>(e + 1u) * dt, val);
|
||||
tsBatchScratch_[e] = base + static_cast<float64>(e) * dt;
|
||||
}
|
||||
}
|
||||
if (nElems > 1u) {
|
||||
lastPktWallS_[s] = wallNowS;
|
||||
WriteSampleBatch(s, tsBatchScratch_, valScratch_, nElems);
|
||||
|
||||
lastPktWallS_[s] = hrt0Sec; /* sender seconds for next dt */
|
||||
lastPktWallValid_[s] = true;
|
||||
accScalarPrevN_[s] = nElems;
|
||||
}
|
||||
}
|
||||
else {
|
||||
@@ -435,14 +483,23 @@ void UDPSourceSession::ParseDataPayload(const uint8 *payload, uint32 size) {
|
||||
* and overlaps the next packet under jitter), elements span
|
||||
* (lastPktWall, wallNow]: the samples were acquired before the
|
||||
* packet arrived, and this keeps ring time strictly monotonic. */
|
||||
if (lastPktWallValid_[s] && (wallNowS > lastPktWallS_[s])) {
|
||||
const float64 dt = (wallNowS - lastPktWallS_[s]) /
|
||||
if (lastPktWallValid_[s] && (wallNowS >= lastPktWallS_[s])) {
|
||||
float64 dt;
|
||||
float64 base;
|
||||
if (wallNowS > lastPktWallS_[s]) {
|
||||
dt = (wallNowS - lastPktWallS_[s]) /
|
||||
static_cast<float64>(nElems);
|
||||
const float64 base = lastPktWallS_[s];
|
||||
for (uint32 e = 0u; e < nElems; e++) {
|
||||
const float64 val = DecodeOneElem(payload, sigOff[s], desc, e);
|
||||
WriteSample(s, e, base + static_cast<float64>(e + 1u) * dt, val);
|
||||
base = lastPktWallS_[s];
|
||||
} else {
|
||||
dt = (desc.samplingRate > 0.0)
|
||||
? (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;
|
||||
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
|
||||
* at payload offset @p off into @p out (dequantised physical values).
|
||||
@@ -253,12 +269,23 @@ private:
|
||||
bool timeSigCalibValid_[UDPSS_MAX_SIGNALS];
|
||||
|
||||
/* 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];
|
||||
bool lastPktWallValid_[UDPSS_MAX_SIGNALS];
|
||||
|
||||
/* Scratch for decoding referenced time-signal arrays (receive thread only). */
|
||||
float64 *timeScratch_;
|
||||
/* Accumulated-scalar timing: HRT counter frequency (local == sender on the
|
||||
* 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_;
|
||||
|
||||
/* 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
|
||||
@@ -11,11 +11,10 @@
|
||||
* Ch1 (1 kHz, 1 V, phase 0)
|
||||
* 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)
|
||||
* Ch4 (50 kHz, 1.5 V, phase 45 deg)
|
||||
*/
|
||||
|
||||
$App = {
|
||||
Class = RealTimeApplication
|
||||
|
||||
@@ -23,7 +22,6 @@ $App = {
|
||||
Class = ReferenceContainer
|
||||
|
||||
// ── Thread1: 1 kHz scalar sines (1 ksps) ────────────────────────────
|
||||
|
||||
+TimerGAM = {
|
||||
Class = IOGAM
|
||||
InputSignals = {
|
||||
@@ -38,8 +36,14 @@ $App = {
|
||||
}
|
||||
}
|
||||
OutputSignals = {
|
||||
Counter = { DataSource = DDB1 Type = uint32 }
|
||||
Time = { DataSource = DDB1 Type = uint32 }
|
||||
Counter = {
|
||||
DataSource = DDB1
|
||||
Type = uint32
|
||||
}
|
||||
Time = {
|
||||
DataSource = DDB1
|
||||
Type = uint32
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,7 +55,10 @@ $App = {
|
||||
Offset = 0.0
|
||||
SamplingRate = 1000.0
|
||||
OutputSignals = {
|
||||
Sine1 = { DataSource = DDB1 Type = float32 }
|
||||
Sine1 = {
|
||||
DataSource = DDB1
|
||||
Type = float32
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,28 +70,54 @@ $App = {
|
||||
Offset = 0.0
|
||||
SamplingRate = 1000.0
|
||||
OutputSignals = {
|
||||
Sine2 = { DataSource = DDB1 Type = float32 }
|
||||
Sine2 = {
|
||||
DataSource = DDB1
|
||||
Type = float32
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+StreamerGAM1 = {
|
||||
Class = IOGAM
|
||||
InputSignals = {
|
||||
Counter = { DataSource = DDB1 Type = uint32 }
|
||||
Time = { DataSource = DDB1 Type = uint32 }
|
||||
Sine1 = { DataSource = DDB1 Type = float32 }
|
||||
Sine2 = { DataSource = DDB1 Type = float32 }
|
||||
Counter = {
|
||||
DataSource = DDB1
|
||||
Type = uint32
|
||||
}
|
||||
Time = {
|
||||
DataSource = DDB1
|
||||
Type = uint32
|
||||
}
|
||||
Sine1 = {
|
||||
DataSource = DDB1
|
||||
Type = float32
|
||||
}
|
||||
Sine2 = {
|
||||
DataSource = DDB1
|
||||
Type = float32
|
||||
}
|
||||
}
|
||||
OutputSignals = {
|
||||
Counter = { DataSource = Streamer1 Type = uint32 }
|
||||
Time = { DataSource = Streamer1 Type = uint32 }
|
||||
Sine1 = { DataSource = Streamer1 Type = float32 }
|
||||
Sine2 = { DataSource = Streamer1 Type = float32 }
|
||||
Counter = {
|
||||
DataSource = Streamer1
|
||||
Type = uint32
|
||||
}
|
||||
Time = {
|
||||
DataSource = Streamer1
|
||||
Type = uint32
|
||||
}
|
||||
Sine1 = {
|
||||
DataSource = Streamer1
|
||||
Type = float32
|
||||
}
|
||||
Sine2 = {
|
||||
DataSource = Streamer1
|
||||
Type = float32
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Thread2: 1 kHz, 1000-element arrays → 1 Msps ───────────────────
|
||||
|
||||
+MedTimerGAM = {
|
||||
Class = IOGAM
|
||||
InputSignals = {
|
||||
@@ -95,7 +128,10 @@ $App = {
|
||||
}
|
||||
}
|
||||
OutputSignals = {
|
||||
Time = { DataSource = DDB2 Type = uint32 }
|
||||
Time = {
|
||||
DataSource = DDB2
|
||||
Type = uint32
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,9 +172,12 @@ $App = {
|
||||
+TimeArrayGAM1 = {
|
||||
Class = TimeArrayGAM
|
||||
SamplingRate = 1000000.0
|
||||
Anchor = FirstSample
|
||||
Anchor = "FirstSample"
|
||||
InputSignals = {
|
||||
Time = { DataSource = DDB2 Type = uint32 }
|
||||
Time = {
|
||||
DataSource = DDB2
|
||||
Type = uint32
|
||||
}
|
||||
}
|
||||
OutputSignals = {
|
||||
TimeArray1 = {
|
||||
@@ -153,19 +192,48 @@ $App = {
|
||||
+StreamerGAM2 = {
|
||||
Class = IOGAM
|
||||
InputSignals = {
|
||||
TimeArray1 = { DataSource = DDB2 Type = uint64 NumberOfDimensions = 1 NumberOfElements = 1000 }
|
||||
Ch1 = { DataSource = DDB2 Type = float32 NumberOfDimensions = 1 NumberOfElements = 1000 }
|
||||
Ch2 = { DataSource = DDB2 Type = float32 NumberOfDimensions = 1 NumberOfElements = 1000 }
|
||||
TimeArray1 = {
|
||||
DataSource = DDB2
|
||||
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 = {
|
||||
TimeArray1 = { DataSource = Streamer2 Type = uint64 NumberOfDimensions = 1 NumberOfElements = 1000 }
|
||||
Ch1 = { DataSource = Streamer2 Type = float32 NumberOfDimensions = 1 NumberOfElements = 1000 }
|
||||
Ch2 = { DataSource = Streamer2 Type = float32 NumberOfDimensions = 1 NumberOfElements = 1000 }
|
||||
TimeArray1 = {
|
||||
DataSource = Streamer2
|
||||
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 ───────────────────
|
||||
|
||||
+FastTimerGAM = {
|
||||
Class = IOGAM
|
||||
InputSignals = {
|
||||
@@ -176,7 +244,10 @@ $App = {
|
||||
}
|
||||
}
|
||||
OutputSignals = {
|
||||
Time = { DataSource = DDB3 Type = uint32 }
|
||||
Time = {
|
||||
DataSource = DDB3
|
||||
Type = uint32
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,9 +288,12 @@ $App = {
|
||||
+TimeArrayGAM2 = {
|
||||
Class = TimeArrayGAM
|
||||
SamplingRate = 5000000.0
|
||||
Anchor = FirstSample
|
||||
Anchor = "FirstSample"
|
||||
InputSignals = {
|
||||
Time = { DataSource = DDB3 Type = uint32 }
|
||||
Time = {
|
||||
DataSource = DDB3
|
||||
Type = uint32
|
||||
}
|
||||
}
|
||||
OutputSignals = {
|
||||
TimeArray2 = {
|
||||
@@ -234,14 +308,44 @@ $App = {
|
||||
+StreamerGAM3 = {
|
||||
Class = IOGAM
|
||||
InputSignals = {
|
||||
TimeArray2 = { DataSource = DDB3 Type = uint64 NumberOfDimensions = 1 NumberOfElements = 1000 }
|
||||
Ch3 = { DataSource = DDB3 Type = float32 NumberOfDimensions = 1 NumberOfElements = 1000 }
|
||||
Ch4 = { DataSource = DDB3 Type = float32 NumberOfDimensions = 1 NumberOfElements = 1000 }
|
||||
TimeArray2 = {
|
||||
DataSource = DDB3
|
||||
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 = {
|
||||
TimeArray2 = { DataSource = Streamer3 Type = uint64 NumberOfDimensions = 1 NumberOfElements = 1000 }
|
||||
Ch3 = { DataSource = Streamer3 Type = float32 NumberOfDimensions = 1 NumberOfElements = 1000 }
|
||||
Ch4 = { DataSource = Streamer3 Type = float32 NumberOfDimensions = 1 NumberOfElements = 1000 }
|
||||
TimeArray2 = {
|
||||
DataSource = Streamer3
|
||||
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
|
||||
DefaultDataSource = DDB1
|
||||
|
||||
+DDB1 = { Class = GAMDataSource }
|
||||
+DDB2 = { Class = GAMDataSource }
|
||||
+DDB3 = { Class = GAMDataSource }
|
||||
+DDB1 = {
|
||||
Class = GAMDataSource
|
||||
}
|
||||
+DDB2 = {
|
||||
Class = GAMDataSource
|
||||
}
|
||||
+DDB3 = {
|
||||
Class = GAMDataSource
|
||||
}
|
||||
|
||||
+Timer = {
|
||||
Class = LinuxTimer
|
||||
SleepNature = "Default"
|
||||
Signals = {
|
||||
Counter = { Type = uint32 }
|
||||
Time = { Type = uint32 }
|
||||
Counter = {
|
||||
Type = uint32
|
||||
}
|
||||
Time = {
|
||||
Type = uint32
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -267,8 +381,12 @@ $App = {
|
||||
Class = LinuxTimer
|
||||
SleepNature = "Default"
|
||||
Signals = {
|
||||
Counter = { Type = uint32 }
|
||||
Time = { Type = uint32 }
|
||||
Counter = {
|
||||
Type = uint32
|
||||
}
|
||||
Time = {
|
||||
Type = uint32
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -276,8 +394,12 @@ $App = {
|
||||
Class = LinuxTimer
|
||||
SleepNature = "Default"
|
||||
Signals = {
|
||||
Counter = { Type = uint32 }
|
||||
Time = { Type = uint32 }
|
||||
Counter = {
|
||||
Type = uint32
|
||||
}
|
||||
Time = {
|
||||
Type = uint32
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -290,10 +412,25 @@ $App = {
|
||||
PublishingMode = "Accumulate"
|
||||
MinRefreshRate = 100
|
||||
Signals = {
|
||||
Counter = { Type = uint32 }
|
||||
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 }
|
||||
Counter = {
|
||||
Type = uint32
|
||||
}
|
||||
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
|
||||
Signals = {
|
||||
TimeArray1 = {
|
||||
Type = uint64 Unit = "ns"
|
||||
NumberOfDimensions = 1 NumberOfElements = 1000
|
||||
Type = uint64
|
||||
Unit = "ns"
|
||||
NumberOfDimensions = 1
|
||||
NumberOfElements = 1000
|
||||
}
|
||||
Ch1 = {
|
||||
Type = float32 Unit = "V"
|
||||
NumberOfDimensions = 1 NumberOfElements = 1000
|
||||
TimeMode = FullArray TimeSignal = TimeArray1
|
||||
Type = float32
|
||||
Unit = "V"
|
||||
NumberOfDimensions = 1
|
||||
NumberOfElements = 1000
|
||||
TimeMode = "FullArray"
|
||||
TimeSignal = TimeArray1
|
||||
}
|
||||
Ch2 = {
|
||||
Type = float32 Unit = "V"
|
||||
NumberOfDimensions = 1 NumberOfElements = 1000
|
||||
TimeMode = FullArray TimeSignal = TimeArray1
|
||||
Type = float32
|
||||
Unit = "V"
|
||||
NumberOfDimensions = 1
|
||||
NumberOfElements = 1000
|
||||
TimeMode = "FullArray"
|
||||
TimeSignal = TimeArray1
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -326,26 +471,36 @@ $App = {
|
||||
Port = 44502
|
||||
MaxPayloadSize = 1400
|
||||
PublishingMode = "Decimate"
|
||||
Ratio = 50
|
||||
Ratio = 5
|
||||
Signals = {
|
||||
TimeArray2 = {
|
||||
Type = uint64 Unit = "ns"
|
||||
NumberOfDimensions = 1 NumberOfElements = 1000
|
||||
Type = uint64
|
||||
Unit = "ns"
|
||||
NumberOfDimensions = 1
|
||||
NumberOfElements = 1000
|
||||
}
|
||||
Ch3 = {
|
||||
Type = float32 Unit = "V"
|
||||
NumberOfDimensions = 1 NumberOfElements = 1000
|
||||
TimeMode = FullArray TimeSignal = TimeArray2
|
||||
Type = float32
|
||||
Unit = "V"
|
||||
NumberOfDimensions = 1
|
||||
NumberOfElements = 1000
|
||||
TimeMode = "FullArray"
|
||||
TimeSignal = TimeArray2
|
||||
}
|
||||
Ch4 = {
|
||||
Type = float32 Unit = "V"
|
||||
NumberOfDimensions = 1 NumberOfElements = 1000
|
||||
TimeMode = FullArray TimeSignal = TimeArray2
|
||||
Type = float32
|
||||
Unit = "V"
|
||||
NumberOfDimensions = 1
|
||||
NumberOfElements = 1000
|
||||
TimeMode = "FullArray"
|
||||
TimeSignal = TimeArray2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+Timings = { Class = TimingDataSource }
|
||||
+Timings = {
|
||||
Class = TimingDataSource
|
||||
}
|
||||
}
|
||||
|
||||
+States = {
|
||||
@@ -358,19 +513,19 @@ $App = {
|
||||
+Thread1 = {
|
||||
Class = RealTimeThread
|
||||
CPUs = 0x1
|
||||
Functions = {TimerGAM SineGAM1 SineGAM2 StreamerGAM1}
|
||||
Functions = { TimerGAM, SineGAM1, SineGAM2, StreamerGAM1 }
|
||||
}
|
||||
|
||||
+Thread2 = {
|
||||
Class = RealTimeThread
|
||||
CPUs = 0x2
|
||||
Functions = {MedTimerGAM SineGAM3 SineGAM4 TimeArrayGAM1 StreamerGAM2}
|
||||
Functions = { MedTimerGAM, SineGAM3, SineGAM4, TimeArrayGAM1, StreamerGAM2 }
|
||||
}
|
||||
|
||||
+Thread3 = {
|
||||
Class = RealTimeThread
|
||||
CPUs = 0x4
|
||||
Functions = {FastTimerGAM SineGAM5 SineGAM6 TimeArrayGAM2 StreamerGAM3}
|
||||
Functions = { FastTimerGAM, SineGAM5, SineGAM6, TimeArrayGAM2, StreamerGAM3 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
@@ -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 — 1 kHz, 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
|
||||
|
||||
Thread 2 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: 54 ms → ∼18 Hz max throughput. Bottlenecks: `FileWriter` async flush (50 ms) and poll sleeps (2 ms).],
|
||||
)
|
||||
|
||||
== Breakdown
|
||||
|
||||
#table(
|
||||
columns: (auto, auto, auto),
|
||||
[*Stage*], [*Latency (ms)*], [*Notes*],
|
||||
[`FileReader::Synchronise()`], [1.0], [Blocking read from OS buffer],
|
||||
[`IOGAM` (memcpy)], [0.1], [24 KB 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], [24 KB copy (6100 floats)],
|
||||
[`FileWriter` (async flush)], [50.0], [Disk I/O, buffer count configurable],
|
||||
[*Total*], [*54.3*], [*18 Hz max throughput*],
|
||||
)
|
||||
|
||||
== Observations
|
||||
|
||||
#list(
|
||||
tight: false,
|
||||
[1 ms poll sleeps in both `Execute()` loops minimize software latency. Total poll overhead: 2 ms.],
|
||||
[`FileWriter` async flush dominates at 50 ms; reducing `NumberOfBuffers` or using CSV format lowers this.],
|
||||
[Maximum theoretical throughput with zero sleeps and sync FileWriter: ∼500 Hz (limited by 24 KB 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 (42 B signal descriptor) + 6100 floats per row (24.4 KB/row).
|
||||
100 rows total, 2.44 MB 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 54 ms, P95 103 ms, P99 129 ms). 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 1 ms. Lower `ResetWait` timeout from 1000 to 100 ms.],
|
||||
[*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`.],
|
||||
)
|
||||
@@ -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()
|
||||
Executable
+278
@@ -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 "=========================================="
|
||||
Executable
+174
@@ -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
@@ -98,8 +98,9 @@ if [[ "$SKIP_BUILD" -eq 0 ]]; then
|
||||
|
||||
if [[ "$START_GUI" -eq 1 ]]; then
|
||||
if [[ -d "${SCRIPT_DIR}/Client/streamhub/build" ]]; then
|
||||
echo "==> Building ImGui client..."
|
||||
cmake --build "${SCRIPT_DIR}/Client/streamhub/build" -j"$(nproc)" 2>&1 | tail -5
|
||||
echo "==> Building ImGui client (a full rebuild can take ~2 min;"
|
||||
echo " ImPlot's implot_items.cpp is one slow -O3 translation unit)..."
|
||||
cmake --build "${SCRIPT_DIR}/Client/streamhub/build" -j"$(nproc)"
|
||||
fi
|
||||
fi
|
||||
|
||||
|
||||
Reference in New Issue
Block a user