feat: standalone C/C++ UDPS client library
Consuming a UDPStreamer feed so far meant either linking MARTe2 (UDPSClient) or writing Go (Common/Client/go/udpsprotocol). Common/Client/c fills the gap for plain C/C++ integrators: two files depending on nothing but libc and BSD sockets, covering the whole receive path — CONNECT, fragment reassembly, CONFIG/DATA decoding with dequantisation, keepalives and silence-triggered reconnect. No threads are spawned; udps_client_poll() does all the work and runs every callback, so it drops into an existing event loop unsynchronised. Verified against run_udp_producer.sh at 1 Msps: unicast (120 MiB, no loss) and multicast with 12-fragment cycles (116k datagrams, no loss). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
e03c60db25
commit
7c5eb31a52
@@ -30,6 +30,9 @@ make -C Source/Components/DataSources/UDPStreamer -f Makefile.gcc
|
||||
cd Common/Client/go && go build ./...
|
||||
cd Client/debugger && go build ./...
|
||||
|
||||
# Standalone C UDPS client library (no MARTe2, libc + BSD sockets only)
|
||||
cd Common/Client/c && make && make cxxcheck
|
||||
|
||||
# ImGui desktop client (not a MARTe2 component; needs SDL2)
|
||||
cd Client/streamhub && cmake -B build -DCMAKE_BUILD_TYPE=Release && cmake --build build
|
||||
|
||||
@@ -81,7 +84,7 @@ Two independent data paths:
|
||||
1. **Streaming path**: `UDPStreamer` DataSource serialises signals each RT cycle to UDPS binary packets (UDP 44500, unicast/multicast) → `StreamHub` (`Source/Applications/StreamHub/`, headless C++ app: ring buffers, LTTB decimation, trigger FSM) → WebSocket 8090 → browser (`Client/udpstreamer`, Go), native ImGui client (`Client/streamhub`), or native Qt client (`Client/streamhub-qt`).
|
||||
2. **Debug path**: `DebugService` patches the `ClassRegistryDatabase` at `Initialise()` so subsequent `ConfigureApplication()` instantiates `DebugBrokerWrapper<T>` around all `MemoryMap*Broker` types — no application changes. RT hot path goes through `DebugServiceI` (abstract singleton in `DebugServiceI.h`) for forcing/tracing/breakpoints. Exposes TCP 8080 (text commands), UDP 8081 (trace telemetry), works with `TcpLogger` on 8082. Web UI: `Client/debugger` (Go).
|
||||
|
||||
**Shared wire format**: `Common/UDP/UDPSProtocol.h` defines the UDPS binary protocol (17-byte packed header, 136-byte signal descriptors, little-endian). It is deliberately MARTe2-free so it's shared by C++ producers (`UDPStreamer`, `DebugService`), the C++ consumer (`Source/Components/Interfaces/UDPStream/UDPSClient`), and the Go decoder (`Common/Client/go/udpsprotocol`). Changes to the protocol must be mirrored across all of these, plus the JS client parsers.
|
||||
**Shared wire format**: `Common/UDP/UDPSProtocol.h` defines the UDPS binary protocol (17-byte packed header, 136-byte signal descriptors, little-endian). It is deliberately MARTe2-free so it's shared by C++ producers (`UDPStreamer`, `DebugService`), the C++ consumer (`Source/Components/Interfaces/UDPStream/UDPSClient`), the Go decoder (`Common/Client/go/udpsprotocol`), and the standalone C client (`Common/Client/c`, which redeclares the constants rather than including this header, so it stays MARTe-free). Changes to the protocol must be mirrored across all of these, plus the JS client parsers.
|
||||
|
||||
**StreamHub WebSocket protocol**: JSON text frames for commands/events, binary frames for data pushes — spec in `ARCHITECTURE.md` §6. The Go hub (`Client/udpstreamer`) and C++ StreamHub implement the identical protocol; both clients (browser JS and ImGui) must stay compatible with both.
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
*.o
|
||||
*.a
|
||||
udps_dump
|
||||
.cxxcheck
|
||||
.cxxcheck.cpp
|
||||
@@ -0,0 +1,46 @@
|
||||
# UDPS C client library — standalone, no MARTe2, no external dependencies.
|
||||
#
|
||||
# make build libudpsclient.a and the example
|
||||
# make example build only the example
|
||||
# make cxxcheck verify the header is usable from C++
|
||||
# make clean
|
||||
|
||||
CC ?= cc
|
||||
CXX ?= c++
|
||||
AR ?= ar
|
||||
CFLAGS ?= -O2 -g
|
||||
WARN = -Wall -Wextra -Wpedantic
|
||||
STD = -std=c99
|
||||
CPPFLAGS += -I.
|
||||
|
||||
# Old glibc (< 2.17) keeps clock_gettime in librt; harmless to add there.
|
||||
LDLIBS ?=
|
||||
|
||||
LIB = libudpsclient.a
|
||||
OBJ = udps_client.o
|
||||
EXAMPLE = udps_dump
|
||||
|
||||
.PHONY: all example cxxcheck clean
|
||||
|
||||
all: $(LIB) $(EXAMPLE)
|
||||
|
||||
$(LIB): $(OBJ)
|
||||
$(AR) rcs $@ $^
|
||||
|
||||
udps_client.o: udps_client.c udps_client.h
|
||||
$(CC) $(STD) $(WARN) $(CFLAGS) $(CPPFLAGS) -c -o $@ $<
|
||||
|
||||
example: $(EXAMPLE)
|
||||
|
||||
$(EXAMPLE): example/udps_dump.c $(LIB)
|
||||
$(CC) $(STD) $(WARN) $(CFLAGS) $(CPPFLAGS) -o $@ $< $(LIB) $(LDLIBS)
|
||||
|
||||
# The header is C++-safe; this target keeps it that way.
|
||||
cxxcheck: udps_client.h
|
||||
echo '#include "udps_client.h"' > .cxxcheck.cpp
|
||||
echo 'int main() { udps_client_config_t c; udps_client_config_init(&c); return 0; }' >> .cxxcheck.cpp
|
||||
$(CXX) -std=c++11 -Wall -Wextra $(CPPFLAGS) -o .cxxcheck .cxxcheck.cpp $(LIB) $(LDLIBS)
|
||||
./.cxxcheck && rm -f .cxxcheck .cxxcheck.cpp
|
||||
|
||||
clean:
|
||||
rm -f $(LIB) $(OBJ) $(EXAMPLE) .cxxcheck .cxxcheck.cpp
|
||||
@@ -0,0 +1,260 @@
|
||||
/**
|
||||
* @file udps_dump.c
|
||||
* @brief Example UDPS client: connects to a UDPStreamer and prints what arrives.
|
||||
*
|
||||
* Build with the Makefile in the parent directory, then for a unicast stream:
|
||||
*
|
||||
* ./udps_dump --host 127.0.0.1 --port 44500
|
||||
*
|
||||
* or, for a multicast one:
|
||||
*
|
||||
* ./udps_dump --host 127.0.0.1 --port 44500 \
|
||||
* --multicast 239.0.0.1 --iface 127.0.0.1
|
||||
*
|
||||
* Ctrl-C prints a summary of what was received.
|
||||
*/
|
||||
|
||||
#define _POSIX_C_SOURCE 200809L
|
||||
|
||||
#include "udps_client.h"
|
||||
|
||||
#include <signal.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
static volatile sig_atomic_t g_stop = 0;
|
||||
|
||||
static void on_sigint(int sig) {
|
||||
(void)sig;
|
||||
g_stop = 1;
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
double print_interval; /**< Seconds between frame printouts. */
|
||||
double last_print;
|
||||
uint64_t frames;
|
||||
uint64_t max_frames;
|
||||
} dump_state_t;
|
||||
|
||||
static double now_wall(void) {
|
||||
struct timespec ts;
|
||||
clock_gettime(CLOCK_REALTIME, &ts);
|
||||
return (double)ts.tv_sec + (double)ts.tv_nsec * 1e-9;
|
||||
}
|
||||
|
||||
static const char *time_mode_name(uint8_t m) {
|
||||
switch (m) {
|
||||
case UDPS_TIME_PACKET: return "packet";
|
||||
case UDPS_TIME_FULL_ARRAY: return "full-array";
|
||||
case UDPS_TIME_FIRST_SAMPLE: return "first-sample";
|
||||
case UDPS_TIME_LAST_SAMPLE: return "last-sample";
|
||||
default: return "?";
|
||||
}
|
||||
}
|
||||
|
||||
static const char *publish_mode_name(uint8_t m) {
|
||||
switch (m) {
|
||||
case UDPS_PUBLISH_STRICT: return "strict";
|
||||
case UDPS_PUBLISH_ACCUMULATE: return "accumulate";
|
||||
case UDPS_PUBLISH_DECIMATE: return "decimate";
|
||||
default: return "?";
|
||||
}
|
||||
}
|
||||
|
||||
static void on_config(const udps_signal_t *sigs, uint32_t n, uint8_t mode,
|
||||
void *user) {
|
||||
uint32_t i;
|
||||
(void)user;
|
||||
printf("\nCONFIG: %u signal(s), publish mode %s\n", (unsigned)n,
|
||||
publish_mode_name(mode));
|
||||
printf(" %-3s %-24s %-8s %-10s %-8s %-10s %s\n", "#", "name", "type",
|
||||
"shape", "unit", "rate[Hz]", "time-mode");
|
||||
for (i = 0u; i < n; i++) {
|
||||
char shape[32];
|
||||
const udps_signal_t *s = &sigs[i];
|
||||
if (s->num_cols > 1u) {
|
||||
snprintf(shape, sizeof shape, "%ux%u", (unsigned)s->num_rows,
|
||||
(unsigned)s->num_cols);
|
||||
} else {
|
||||
snprintf(shape, sizeof shape, "%u",
|
||||
(unsigned)udps_signal_num_elements(s));
|
||||
}
|
||||
printf(" %-3u %-24s %-8s %-10s %-8s %-10.6g %s%s\n", (unsigned)i,
|
||||
s->name, udps_type_name(s->type_code), shape,
|
||||
(s->unit[0] != '\0') ? s->unit : "-", s->sampling_rate,
|
||||
time_mode_name(s->time_mode),
|
||||
(s->quant_type != UDPS_QUANT_NONE) ? " (quantised)" : "");
|
||||
}
|
||||
fflush(stdout);
|
||||
}
|
||||
|
||||
static void on_data(const udps_frame_t *f, void *user) {
|
||||
dump_state_t *st = (dump_state_t *)user;
|
||||
uint32_t i;
|
||||
double now;
|
||||
|
||||
st->frames++;
|
||||
now = now_wall();
|
||||
if ((now - st->last_print) < st->print_interval) {
|
||||
return; /* Streams run far faster than a terminal can be read. */
|
||||
}
|
||||
st->last_print = now;
|
||||
|
||||
printf("\nframe #%lu t=%.6f samples=%u (%lu frames so far)\n",
|
||||
(unsigned long)f->counter, f->recv_time, (unsigned)f->num_samples,
|
||||
(unsigned long)st->frames);
|
||||
for (i = 0u; i < f->num_signals; i++) {
|
||||
const double *v = f->values[i].values;
|
||||
uint32_t cnt = f->values[i].count;
|
||||
double lo, hi;
|
||||
uint32_t k;
|
||||
if (cnt == 0u) {
|
||||
continue;
|
||||
}
|
||||
lo = hi = v[0];
|
||||
for (k = 1u; k < cnt; k++) {
|
||||
if (v[k] < lo) {
|
||||
lo = v[k];
|
||||
}
|
||||
if (v[k] > hi) {
|
||||
hi = v[k];
|
||||
}
|
||||
}
|
||||
printf(" %-24s n=%-6u first=%-12.6g last=%-12.6g min=%-12.6g max=%-12.6g %s\n",
|
||||
f->signals[i].name, (unsigned)cnt, v[0], v[cnt - 1u], lo, hi,
|
||||
f->signals[i].unit);
|
||||
}
|
||||
fflush(stdout);
|
||||
}
|
||||
|
||||
static void on_event(udps_event_t ev, const char *detail, void *user) {
|
||||
(void)user;
|
||||
switch (ev) {
|
||||
case UDPS_EVENT_CONNECTED:
|
||||
printf("[connected to %s]\n", detail ? detail : "");
|
||||
break;
|
||||
case UDPS_EVENT_DISCONNECTED:
|
||||
printf("[disconnected: %s]\n", detail ? detail : "");
|
||||
break;
|
||||
case UDPS_EVENT_ERROR:
|
||||
fprintf(stderr, "[error] %s\n", detail ? detail : "");
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
fflush(stdout);
|
||||
}
|
||||
|
||||
static void usage(const char *argv0) {
|
||||
printf("Usage: %s --host ADDR --port N [options]\n"
|
||||
"\n"
|
||||
" --host ADDR server address (default 127.0.0.1)\n"
|
||||
" --port N server UDP port, or TCP control port in multicast\n"
|
||||
" mode (default 44500)\n"
|
||||
" --multicast GROUP join GROUP for data instead of unicast\n"
|
||||
" --iface ADDR local interface address for the multicast join\n"
|
||||
" --data-port N multicast data port (default: --port + 1)\n"
|
||||
" --silence SEC reconnect after SEC without data (default 1, 0 off)\n"
|
||||
" --interval SEC seconds between printouts (default 1)\n"
|
||||
" --frames N exit after N frames (default: run until Ctrl-C)\n"
|
||||
" --help this text\n",
|
||||
argv0);
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
udps_client_config_t cfg;
|
||||
udps_client_t *cli;
|
||||
dump_state_t st;
|
||||
udps_stats_t stats;
|
||||
struct sigaction sa;
|
||||
const char *host = "127.0.0.1";
|
||||
int i;
|
||||
|
||||
udps_client_config_init(&cfg);
|
||||
cfg.server_port = 44500u;
|
||||
|
||||
memset(&st, 0, sizeof st);
|
||||
st.print_interval = 1.0;
|
||||
|
||||
for (i = 1; i < argc; i++) {
|
||||
const char *a = argv[i];
|
||||
const char *next = (i + 1 < argc) ? argv[i + 1] : NULL;
|
||||
if (strcmp(a, "--help") == 0) {
|
||||
usage(argv[0]);
|
||||
return 0;
|
||||
}
|
||||
if (next == NULL) {
|
||||
fprintf(stderr, "missing value for %s\n", a);
|
||||
return 2;
|
||||
}
|
||||
if (strcmp(a, "--host") == 0) {
|
||||
host = next;
|
||||
} else if (strcmp(a, "--port") == 0) {
|
||||
cfg.server_port = (uint16_t)atoi(next);
|
||||
} else if (strcmp(a, "--multicast") == 0) {
|
||||
cfg.multicast_group = next;
|
||||
} else if (strcmp(a, "--iface") == 0) {
|
||||
cfg.interface_addr = next;
|
||||
} else if (strcmp(a, "--data-port") == 0) {
|
||||
cfg.data_port = (uint16_t)atoi(next);
|
||||
} else if (strcmp(a, "--silence") == 0) {
|
||||
cfg.silence_timeout_s = atof(next);
|
||||
} else if (strcmp(a, "--interval") == 0) {
|
||||
st.print_interval = atof(next);
|
||||
} else if (strcmp(a, "--frames") == 0) {
|
||||
st.max_frames = (uint64_t)strtoull(next, NULL, 10);
|
||||
} else {
|
||||
fprintf(stderr, "unknown option %s\n", a);
|
||||
usage(argv[0]);
|
||||
return 2;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
cfg.server_addr = host;
|
||||
|
||||
cli = udps_client_create(&cfg);
|
||||
if (cli == NULL) {
|
||||
fprintf(stderr, "could not create client for %s:%u\n", host,
|
||||
(unsigned)cfg.server_port);
|
||||
return 1;
|
||||
}
|
||||
udps_client_set_callbacks(cli, on_config, on_data, on_event, &st);
|
||||
|
||||
memset(&sa, 0, sizeof sa);
|
||||
sa.sa_handler = on_sigint;
|
||||
(void)sigaction(SIGINT, &sa, NULL);
|
||||
(void)sigaction(SIGTERM, &sa, NULL);
|
||||
|
||||
printf("listening to %s:%u%s%s ... (Ctrl-C to stop)\n", host,
|
||||
(unsigned)cfg.server_port,
|
||||
cfg.multicast_group ? " via multicast " : "",
|
||||
cfg.multicast_group ? cfg.multicast_group : "");
|
||||
|
||||
while (!g_stop && (st.max_frames == 0u || st.frames < st.max_frames)) {
|
||||
/* All the work — connecting, receiving, decoding, reconnecting — and
|
||||
* every callback happens inside this call. */
|
||||
(void)udps_client_poll(cli, 200);
|
||||
}
|
||||
|
||||
udps_client_stats(cli, &stats);
|
||||
printf("\n--- summary ---\n"
|
||||
"packets %lu\n"
|
||||
"bytes %.1f MiB\n"
|
||||
"frames %lu\n"
|
||||
"configs %lu\n"
|
||||
"gaps %lu (datagrams lost)\n"
|
||||
"dropped %lu (fragments)\n"
|
||||
"reconnects %lu\n",
|
||||
(unsigned long)stats.packets_received,
|
||||
(double)stats.bytes_received / (1024.0 * 1024.0),
|
||||
(unsigned long)stats.frames_delivered,
|
||||
(unsigned long)stats.config_updates,
|
||||
(unsigned long)stats.counter_gaps,
|
||||
(unsigned long)stats.fragments_dropped,
|
||||
(unsigned long)stats.reconnects);
|
||||
|
||||
udps_client_destroy(cli);
|
||||
return 0;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,324 @@
|
||||
#ifndef UDPS_CLIENT_H
|
||||
#define UDPS_CLIENT_H
|
||||
|
||||
/**
|
||||
* @file udps_client.h
|
||||
* @brief Standalone UDPS (UDPStreamer) receiver library — C99, no MARTe2.
|
||||
*
|
||||
* Depends only on libc and BSD sockets, so it can be dropped into any C or C++
|
||||
* program that needs to consume a UDPStreamer / DebugService stream. The wire
|
||||
* format is specified in Docs/Protocol.md; the library reference (and a worked
|
||||
* example) is Docs/UDPS-C-Client.md.
|
||||
*
|
||||
* Usage in one paragraph: fill a udps_client_config_t, create a client, install
|
||||
* callbacks, then call udps_client_poll() in a loop. The client owns the
|
||||
* connection state machine — it sends CONNECT, reassembles fragmented packets,
|
||||
* decodes CONFIG and DATA, sends keepalives, and reconnects when the server
|
||||
* goes silent. Nothing is done behind your back: no threads are created and
|
||||
* every callback runs inside your call to udps_client_poll().
|
||||
*
|
||||
* Threading: a udps_client_t must be used from one thread at a time.
|
||||
*/
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/*---------------------------------------------------------------------------*/
|
||||
/* Protocol constants */
|
||||
/*---------------------------------------------------------------------------*/
|
||||
|
||||
/** Magic number: ASCII 'UDPS' stored little-endian. */
|
||||
#define UDPS_MAGIC 0x53504455u
|
||||
|
||||
/** Size of the packed packet header on the wire. */
|
||||
#define UDPS_HEADER_SIZE 17u
|
||||
|
||||
/** Size of one serialised signal descriptor in a CONFIG payload. */
|
||||
#define UDPS_SIGNAL_DESC_SIZE 136u
|
||||
|
||||
/** Value of udps_signal_t::time_signal_idx when the signal has no time reference. */
|
||||
#define UDPS_NO_TIME_SIGNAL 0xFFFFFFFFu
|
||||
|
||||
/** Upper bound on elements per signal; larger descriptors are rejected. */
|
||||
#define UDPS_MAX_ELEMENTS (1u << 20)
|
||||
|
||||
/** Packet types (udps_header_t::type). */
|
||||
enum {
|
||||
UDPS_PKT_DATA = 0, /**< Server -> client: signal samples. */
|
||||
UDPS_PKT_CONFIG = 1, /**< Server -> client: signal metadata. */
|
||||
UDPS_PKT_ACK = 2, /**< Client -> server: keepalive. */
|
||||
UDPS_PKT_CONNECT = 3, /**< Client -> server: open a session. */
|
||||
UDPS_PKT_DISCONNECT = 4 /**< Either direction: close a session. */
|
||||
};
|
||||
|
||||
/** Sample type codes (udps_signal_t::type_code). */
|
||||
enum {
|
||||
UDPS_T_UINT8 = 0,
|
||||
UDPS_T_INT8 = 1,
|
||||
UDPS_T_UINT16 = 2,
|
||||
UDPS_T_INT16 = 3,
|
||||
UDPS_T_UINT32 = 4,
|
||||
UDPS_T_INT32 = 5,
|
||||
UDPS_T_UINT64 = 6,
|
||||
UDPS_T_INT64 = 7,
|
||||
UDPS_T_FLOAT32 = 8,
|
||||
UDPS_T_FLOAT64 = 9,
|
||||
UDPS_T_UNKNOWN = 255
|
||||
};
|
||||
|
||||
/** Quantisation codes (udps_signal_t::quant_type). */
|
||||
enum {
|
||||
UDPS_QUANT_NONE = 0, /**< Raw values in the signal's own type. */
|
||||
UDPS_QUANT_UINT8 = 1, /**< [range_min, range_max] mapped onto uint8. */
|
||||
UDPS_QUANT_INT8 = 2,
|
||||
UDPS_QUANT_UINT16 = 3,
|
||||
UDPS_QUANT_INT16 = 4
|
||||
};
|
||||
|
||||
/** Time-reference modes (udps_signal_t::time_mode). */
|
||||
enum {
|
||||
UDPS_TIME_PACKET = 0, /**< No per-element time; use packet arrival. */
|
||||
UDPS_TIME_FULL_ARRAY = 1, /**< The time signal carries one stamp per element. */
|
||||
UDPS_TIME_FIRST_SAMPLE = 2, /**< Time signal (scalar) stamps element 0. */
|
||||
UDPS_TIME_LAST_SAMPLE = 3 /**< Time signal (scalar) stamps element N-1. */
|
||||
};
|
||||
|
||||
/** Publishing modes (udps_frame_t::publish_mode). */
|
||||
enum {
|
||||
UDPS_PUBLISH_STRICT = 0, /**< One packet per RT cycle. */
|
||||
UDPS_PUBLISH_ACCUMULATE = 1, /**< A batch of cycles per packet. */
|
||||
UDPS_PUBLISH_DECIMATE = 2 /**< One packet every N cycles. */
|
||||
};
|
||||
|
||||
/*---------------------------------------------------------------------------*/
|
||||
/* Data model */
|
||||
/*---------------------------------------------------------------------------*/
|
||||
|
||||
/** Decoded 17-byte packet header. */
|
||||
typedef struct {
|
||||
uint32_t magic;
|
||||
uint8_t type;
|
||||
uint32_t counter; /**< Same for every fragment of one update. */
|
||||
uint16_t fragment_idx;
|
||||
uint16_t total_fragments; /**< 1 when the update fits in one datagram. */
|
||||
uint32_t payload_bytes;
|
||||
} udps_header_t;
|
||||
|
||||
/** Metadata for one streamed signal, as carried by the CONFIG payload. */
|
||||
typedef struct {
|
||||
char name[65]; /**< NUL-terminated. */
|
||||
uint8_t type_code; /**< UDPS_T_*. */
|
||||
uint8_t quant_type; /**< UDPS_QUANT_*. */
|
||||
uint8_t num_dimensions; /**< 0 scalar, 1 vector, 2 matrix. */
|
||||
uint32_t num_rows;
|
||||
uint32_t num_cols;
|
||||
double range_min; /**< Physical range, used to dequantise. */
|
||||
double range_max;
|
||||
uint8_t time_mode; /**< UDPS_TIME_*. */
|
||||
double sampling_rate; /**< Hz; 0 when unknown. */
|
||||
uint32_t time_signal_idx;/**< Index into the signal list, or UDPS_NO_TIME_SIGNAL. */
|
||||
char unit[33]; /**< NUL-terminated. */
|
||||
} udps_signal_t;
|
||||
|
||||
/**
|
||||
* @brief Decoded values of one signal within a frame.
|
||||
*
|
||||
* Values are always physical doubles: quantised signals are already expanded
|
||||
* back onto [range_min, range_max]. @c count is @c num_samples for a scalar
|
||||
* signal in Accumulate mode (one value per batched cycle) and the signal's
|
||||
* element count in every other case.
|
||||
*/
|
||||
typedef struct {
|
||||
const double *values;
|
||||
uint32_t count;
|
||||
} udps_signal_values_t;
|
||||
|
||||
/** One fully decoded DATA packet. */
|
||||
typedef struct {
|
||||
uint32_t counter; /**< Packet counter; gaps mean lost datagrams. */
|
||||
uint64_t hrt; /**< Producer's high-resolution timer at send. */
|
||||
double recv_time; /**< Wall-clock seconds (CLOCK_REALTIME) at arrival. */
|
||||
uint8_t publish_mode; /**< UDPS_PUBLISH_*. */
|
||||
uint32_t num_samples; /**< Batched cycles; 1 unless Accumulate. */
|
||||
uint32_t num_signals;
|
||||
const udps_signal_t *signals; /**< num_signals entries, CONFIG order. */
|
||||
const udps_signal_values_t *values; /**< num_signals entries, same order. */
|
||||
} udps_frame_t;
|
||||
|
||||
/** Connection lifecycle events reported through udps_event_cb. */
|
||||
typedef enum {
|
||||
UDPS_EVENT_CONNECTED, /**< Sockets are up and CONNECT was sent. */
|
||||
UDPS_EVENT_DISCONNECTED, /**< Session dropped; the client will retry. */
|
||||
UDPS_EVENT_ERROR /**< Recoverable problem; detail says what. */
|
||||
} udps_event_t;
|
||||
|
||||
/** Cumulative counters, never reset. */
|
||||
typedef struct {
|
||||
uint64_t packets_received; /**< Datagrams (and TCP frames) accepted. */
|
||||
uint64_t bytes_received;
|
||||
uint64_t frames_delivered; /**< DATA packets decoded and handed to you. */
|
||||
uint64_t config_updates;
|
||||
uint64_t fragments_dropped; /**< Duplicate, stale or unplaceable fragments. */
|
||||
uint64_t counter_gaps; /**< DATA packets missing from the sequence. */
|
||||
uint64_t reconnects;
|
||||
} udps_stats_t;
|
||||
|
||||
/*---------------------------------------------------------------------------*/
|
||||
/* Client */
|
||||
/*---------------------------------------------------------------------------*/
|
||||
|
||||
typedef struct udps_client udps_client_t;
|
||||
|
||||
/** Called whenever a CONFIG packet redefines the signal set. */
|
||||
typedef void (*udps_config_cb)(const udps_signal_t *signals,
|
||||
uint32_t num_signals,
|
||||
uint8_t publish_mode,
|
||||
void *user);
|
||||
|
||||
/**
|
||||
* @brief Called for every decoded DATA packet.
|
||||
*
|
||||
* The frame and everything it points at are owned by the client and are only
|
||||
* valid until the callback returns — copy anything you need to keep.
|
||||
*/
|
||||
typedef void (*udps_data_cb)(const udps_frame_t *frame, void *user);
|
||||
|
||||
/** Called on connection state changes and on recoverable errors. */
|
||||
typedef void (*udps_event_cb)(udps_event_t event, const char *detail, void *user);
|
||||
|
||||
/**
|
||||
* @brief Transport configuration.
|
||||
*
|
||||
* Zero-initialise with udps_client_config_init(), then override what you need.
|
||||
* Set @c multicast_group to switch from unicast to multicast: in unicast the
|
||||
* client sends CONNECT over UDP and receives everything on its ephemeral port;
|
||||
* in multicast it joins the group for DATA and opens a TCP control connection
|
||||
* to @c server_port for CONNECT and CONFIG.
|
||||
*/
|
||||
typedef struct {
|
||||
const char *server_addr; /**< IPv4 dotted quad. Required. */
|
||||
uint16_t server_port; /**< UDP port (unicast) or TCP port (multicast). Required. */
|
||||
const char *multicast_group;/**< IPv4 group; NULL selects unicast. */
|
||||
const char *interface_addr; /**< Local IPv4 of the interface to join on. NULL = default route. */
|
||||
uint16_t data_port; /**< Multicast data port; 0 means server_port + 1. */
|
||||
double silence_timeout_s; /**< Reconnect after this long without data. 0 disables. */
|
||||
double reconnect_delay_s; /**< Wait between reconnect attempts. */
|
||||
double keepalive_interval_s;/**< Unicast ACK period. 0 disables. */
|
||||
uint32_t recv_buffer_bytes; /**< SO_RCVBUF; large bursts need a large value. */
|
||||
uint32_t max_packet_bytes; /**< Ceiling on one reassembled payload. */
|
||||
} udps_client_config_t;
|
||||
|
||||
/** Fills @p cfg with the defaults documented in Docs/UDPS-C-Client.md. */
|
||||
void udps_client_config_init(udps_client_config_t *cfg);
|
||||
|
||||
/**
|
||||
* @brief Creates a client. No socket is opened until the first poll.
|
||||
* @return NULL if @p cfg is invalid or memory ran out.
|
||||
*/
|
||||
udps_client_t *udps_client_create(const udps_client_config_t *cfg);
|
||||
|
||||
/** Closes the session (sending DISCONNECT if connected) and frees the client. */
|
||||
void udps_client_destroy(udps_client_t *client);
|
||||
|
||||
/** Installs the callbacks. Any of them may be NULL. */
|
||||
void udps_client_set_callbacks(udps_client_t *client,
|
||||
udps_config_cb on_config,
|
||||
udps_data_cb on_data,
|
||||
udps_event_cb on_event,
|
||||
void *user);
|
||||
|
||||
/**
|
||||
* @brief Drives the client: connects if needed, then waits for and processes
|
||||
* packets for at most @p timeout_ms milliseconds.
|
||||
*
|
||||
* Callbacks fire from inside this call. A negative @p timeout_ms blocks until
|
||||
* something happens. Call it in a loop; it is the only function that does work.
|
||||
*
|
||||
* @return the number of packets processed (0 on timeout), or -1 if the session
|
||||
* broke. -1 is not fatal: the next call retries after reconnect_delay_s.
|
||||
*/
|
||||
int udps_client_poll(udps_client_t *client, int timeout_ms);
|
||||
|
||||
/** Non-zero once the sockets are up (which does not yet imply CONFIG arrived). */
|
||||
int udps_client_is_connected(const udps_client_t *client);
|
||||
|
||||
/**
|
||||
* @brief The current signal set, or NULL before the first CONFIG.
|
||||
* @param num_signals Out; may be NULL.
|
||||
*/
|
||||
const udps_signal_t *udps_client_signals(const udps_client_t *client,
|
||||
uint32_t *num_signals);
|
||||
|
||||
/** The publishing mode from the last CONFIG (UDPS_PUBLISH_*). */
|
||||
uint8_t udps_client_publish_mode(const udps_client_t *client);
|
||||
|
||||
/** Copies the counters into @p out. */
|
||||
void udps_client_stats(const udps_client_t *client, udps_stats_t *out);
|
||||
|
||||
/** Human-readable description of the last failure. Never NULL. */
|
||||
const char *udps_client_last_error(const udps_client_t *client);
|
||||
|
||||
/*---------------------------------------------------------------------------*/
|
||||
/* Stateless helpers */
|
||||
/*---------------------------------------------------------------------------*/
|
||||
|
||||
/** Elements in one sample of @p signal (rows x cols, at least 1). */
|
||||
uint32_t udps_signal_num_elements(const udps_signal_t *signal);
|
||||
|
||||
/** Short name of a type code, e.g. "float32". Never NULL. */
|
||||
const char *udps_type_name(uint8_t type_code);
|
||||
|
||||
/**
|
||||
* @brief Decodes a packet header.
|
||||
* @return 0 on success, -1 if @p len is too small or the magic is wrong.
|
||||
*/
|
||||
int udps_parse_header(const void *buf, size_t len, udps_header_t *out);
|
||||
|
||||
/**
|
||||
* @brief Decodes a reassembled CONFIG payload.
|
||||
* @param signals Out array of at most @p max_signals entries.
|
||||
* @param num_signals Out; the number actually written.
|
||||
* @param publish_mode Out; may be NULL.
|
||||
* @return 0 on success, -1 if the payload is malformed or does not fit.
|
||||
*/
|
||||
int udps_parse_config(const void *payload,
|
||||
size_t len,
|
||||
udps_signal_t *signals,
|
||||
uint32_t max_signals,
|
||||
uint32_t *num_signals,
|
||||
uint8_t *publish_mode);
|
||||
|
||||
/**
|
||||
* @brief One value out of a frame.
|
||||
* @param sample Accumulate batch slot; ignored for non-scalar signals.
|
||||
* @param elem Element within the sample; ignored for accumulated scalars.
|
||||
* @return the value, or 0.0 if any index is out of range.
|
||||
*/
|
||||
double udps_frame_value(const udps_frame_t *frame,
|
||||
uint32_t signal_idx,
|
||||
uint32_t sample,
|
||||
uint32_t elem);
|
||||
|
||||
/**
|
||||
* @brief Arrival-anchored estimate of the wall-clock time of one element.
|
||||
*
|
||||
* Exact only for streams that declare a sampling rate: the packet is assumed to
|
||||
* have arrived as its last element was produced, and earlier elements are dated
|
||||
* backwards by 1/sampling_rate. Signals with UDPS_TIME_PACKET, or without a
|
||||
* sampling rate, all report the arrival time. When the stream carries a time
|
||||
* signal (time_signal_idx != UDPS_NO_TIME_SIGNAL) that signal is the accurate
|
||||
* source — read it like any other signal instead of using this helper.
|
||||
*/
|
||||
double udps_frame_element_time(const udps_frame_t *frame,
|
||||
uint32_t signal_idx,
|
||||
uint32_t elem);
|
||||
|
||||
#ifdef __cplusplus
|
||||
} /* extern "C" */
|
||||
#endif
|
||||
|
||||
#endif /* UDPS_CLIENT_H */
|
||||
@@ -0,0 +1,287 @@
|
||||
# UDPS C Client Library
|
||||
|
||||
`Common/Client/c/` is a standalone receiver for the UDPS streaming protocol: it connects to a
|
||||
`UDPStreamer` DataSource (or any other UDPS producer, such as `DebugService`), decodes the
|
||||
signals, and hands them to your callbacks as plain `double`s.
|
||||
|
||||
It has **no MARTe2 dependency** and no third-party dependencies at all — just libc and BSD
|
||||
sockets. Two files, `udps_client.h` and `udps_client.c`, drop into any C or C++ project.
|
||||
|
||||
The wire format itself is specified in [Protocol.md](Protocol.md); this document covers the
|
||||
library. The producer side is documented in [UDPStreamer.md](UDPStreamer.md).
|
||||
|
||||
---
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
cd Common/Client/c
|
||||
make # libudpsclient.a + the udps_dump example
|
||||
make cxxcheck # verifies the header compiles and links from C++
|
||||
make clean
|
||||
```
|
||||
|
||||
Or just add the two files to your own build:
|
||||
|
||||
```bash
|
||||
cc -std=c99 -O2 -c udps_client.c
|
||||
```
|
||||
|
||||
Requirements: a C99 compiler and POSIX sockets. On glibc older than 2.17 add `-lrt`
|
||||
(`clock_gettime` lived in librt back then). The header is wrapped in `extern "C"`, so C++
|
||||
callers include it directly.
|
||||
|
||||
---
|
||||
|
||||
## Quick start
|
||||
|
||||
```c
|
||||
#include "udps_client.h"
|
||||
#include <stdio.h>
|
||||
|
||||
static void on_data(const udps_frame_t *f, void *user) {
|
||||
(void)user;
|
||||
/* Signals are in CONFIG order; values are already physical doubles. */
|
||||
printf("#%u %s = %g\n", f->counter, f->signals[0].name, f->values[0].values[0]);
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
udps_client_config_t cfg;
|
||||
udps_client_t *cli;
|
||||
|
||||
udps_client_config_init(&cfg);
|
||||
cfg.server_addr = "127.0.0.1";
|
||||
cfg.server_port = 44500;
|
||||
|
||||
cli = udps_client_create(&cfg);
|
||||
udps_client_set_callbacks(cli, NULL, on_data, NULL, NULL);
|
||||
|
||||
for (;;) {
|
||||
udps_client_poll(cli, 200); /* connects, receives, decodes, reconnects */
|
||||
}
|
||||
udps_client_destroy(cli);
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
`udps_client_poll()` is the only function that does work. It never spawns a thread, and every
|
||||
callback runs inside it — so if your program already has an event loop, call it from there and
|
||||
you need no synchronisation at all. A client must be used from one thread at a time.
|
||||
|
||||
---
|
||||
|
||||
## Connection model
|
||||
|
||||
The library implements both transports of the protocol and picks one from the configuration:
|
||||
|
||||
| | Unicast (`multicast_group == NULL`) | Multicast (`multicast_group` set) |
|
||||
|---|---|---|
|
||||
| CONNECT | UDP datagram to `server_addr:server_port` | over a TCP connection to `server_addr:server_port` |
|
||||
| CONFIG | UDP, back to the client's ephemeral port | over the same TCP connection |
|
||||
| DATA | UDP, same ephemeral port | UDP multicast on `data_port` |
|
||||
| Keepalive | ACK every `keepalive_interval_s` | not needed (the TCP session is the liveness signal) |
|
||||
|
||||
In multicast mode the group is joined *before* CONNECT is sent, because the server multicasts
|
||||
CONFIG as soon as it sees a client — a group joined afterwards would miss it.
|
||||
|
||||
The client reconnects on its own: if nothing arrives for `silence_timeout_s` it sends
|
||||
DISCONNECT, closes the sockets, waits `reconnect_delay_s`, and starts over. `udps_client_poll()`
|
||||
returns `-1` when that happens, which is informational, not fatal.
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
Always start from `udps_client_config_init()` — it fills in the defaults below — then override
|
||||
what you need. Strings are copied into the client, so they need not outlive `udps_client_create()`.
|
||||
|
||||
| Field | Default | Meaning |
|
||||
|---|---|---|
|
||||
| `server_addr` | — (required) | Server IPv4 address; a hostname is resolved if it is not a dotted quad. |
|
||||
| `server_port` | — (required) | Server UDP port, or the TCP control port in multicast mode. |
|
||||
| `multicast_group` | `NULL` | IPv4 group to join. Non-`NULL` selects the multicast transport. |
|
||||
| `interface_addr` | `NULL` | Local IPv4 **address** (not a name, e.g. `"127.0.0.1"`) of the interface to join on. Defaults to the default route, which silently receives nothing if the server sends elsewhere. |
|
||||
| `data_port` | `server_port + 1` | Multicast data port. Must match the producer's `DataPort`. |
|
||||
| `silence_timeout_s` | `1.0` | Reconnect after this long without data. `0` disables the check — use it for streams that are idle by design. |
|
||||
| `reconnect_delay_s` | `2.0` | Wait between reconnection attempts. |
|
||||
| `keepalive_interval_s` | `15.0` | Unicast ACK period. The server evicts silent clients after its `ClientTimeout` (30 s by default). `0` disables. |
|
||||
| `recv_buffer_bytes` | 4 MiB | `SO_RCVBUF`. The Linux default (~208 KiB) is overrun by fast producers and the kernel drops datagrams silently. |
|
||||
| `max_packet_bytes` | 1 MiB | Ceiling on one reassembled payload; a reassembly buffer of this size is allocated per in-flight update (4 at most). |
|
||||
|
||||
---
|
||||
|
||||
## API
|
||||
|
||||
### Lifecycle
|
||||
|
||||
```c
|
||||
void udps_client_config_init(udps_client_config_t *cfg);
|
||||
udps_client_t *udps_client_create(const udps_client_config_t *cfg);
|
||||
void udps_client_set_callbacks(udps_client_t *c, udps_config_cb, udps_data_cb,
|
||||
udps_event_cb, void *user);
|
||||
int udps_client_poll(udps_client_t *c, int timeout_ms);
|
||||
void udps_client_destroy(udps_client_t *c);
|
||||
```
|
||||
|
||||
`udps_client_create()` returns `NULL` on a bad address or an invalid configuration; no socket is
|
||||
opened until the first poll. `udps_client_poll()` returns the number of packets processed, `0` on
|
||||
timeout, or `-1` if the session broke — pass a negative `timeout_ms` to block. `destroy` sends
|
||||
DISCONNECT before closing.
|
||||
|
||||
### Callbacks
|
||||
|
||||
```c
|
||||
void on_config(const udps_signal_t *signals, uint32_t n, uint8_t publish_mode, void *user);
|
||||
void on_data (const udps_frame_t *frame, void *user);
|
||||
void on_event (udps_event_t event, const char *detail, void *user);
|
||||
```
|
||||
|
||||
`on_config` fires on every CONFIG packet: the signal set can change at runtime, so treat it as a
|
||||
reset of everything you cached. `on_event` reports `UDPS_EVENT_CONNECTED`,
|
||||
`UDPS_EVENT_DISCONNECTED` and `UDPS_EVENT_ERROR` with a human-readable `detail`.
|
||||
|
||||
> **The frame and everything it points at are owned by the client and are valid only until
|
||||
> `on_data` returns.** The decode buffers are reused by the next packet. Copy what you keep.
|
||||
|
||||
### Inspection
|
||||
|
||||
```c
|
||||
int udps_client_is_connected(const udps_client_t *c);
|
||||
const udps_signal_t *udps_client_signals(const udps_client_t *c, uint32_t *n);
|
||||
uint8_t udps_client_publish_mode(const udps_client_t *c);
|
||||
void udps_client_stats(const udps_client_t *c, udps_stats_t *out);
|
||||
const char *udps_client_last_error(const udps_client_t *c);
|
||||
```
|
||||
|
||||
### Helpers
|
||||
|
||||
```c
|
||||
uint32_t udps_signal_num_elements(const udps_signal_t *s);
|
||||
const char *udps_type_name(uint8_t type_code);
|
||||
int udps_parse_header(const void *buf, size_t len, udps_header_t *out);
|
||||
int udps_parse_config(const void *payload, size_t len, udps_signal_t *sigs,
|
||||
uint32_t max_signals, uint32_t *n, uint8_t *publish_mode);
|
||||
double udps_frame_value(const udps_frame_t *f, uint32_t sig, uint32_t sample, uint32_t elem);
|
||||
double udps_frame_element_time(const udps_frame_t *f, uint32_t sig, uint32_t elem);
|
||||
```
|
||||
|
||||
`udps_parse_header` and `udps_parse_config` are stateless and socket-free, so captured or
|
||||
replayed traffic can be decoded without a client.
|
||||
|
||||
---
|
||||
|
||||
## Reading a frame
|
||||
|
||||
```c
|
||||
typedef struct {
|
||||
uint32_t counter; /* gaps in this sequence are lost datagrams */
|
||||
uint64_t hrt; /* producer's high-resolution timer at send */
|
||||
double recv_time; /* CLOCK_REALTIME seconds at arrival */
|
||||
uint8_t publish_mode;
|
||||
uint32_t num_samples; /* batched RT cycles; 1 unless Accumulate */
|
||||
uint32_t num_signals;
|
||||
const udps_signal_t *signals; /* CONFIG order */
|
||||
const udps_signal_values_t *values; /* same order */
|
||||
} udps_frame_t;
|
||||
```
|
||||
|
||||
`values[i].values` is an array of `values[i].count` physical `double`s. Quantised signals are
|
||||
already expanded back onto `[range_min, range_max]`, and integer types are widened — the decoded
|
||||
form does not depend on the wire type, so a consumer need not branch on `type_code` at all.
|
||||
|
||||
**Element count.** `count` is the signal's element count (`num_rows × num_cols`), *except* for a
|
||||
scalar signal in Accumulate mode, where the producer batches several RT cycles into one packet
|
||||
and `count == num_samples` — one value per cycle. Arrays are not batched: they appear once and
|
||||
apply to the whole packet. `udps_frame_value(f, sig, sample, elem)` applies that rule for you.
|
||||
|
||||
**Timestamps.** The protocol does not put a timestamp on every element; how to date them depends
|
||||
on the signal's `time_mode` (see [Protocol.md](Protocol.md#time-mode-codes)):
|
||||
|
||||
| `time_mode` | Where the time comes from |
|
||||
|---|---|
|
||||
| `UDPS_TIME_PACKET` | No per-element time. Use `recv_time`. |
|
||||
| `UDPS_TIME_FULL_ARRAY` | The signal at `time_signal_idx` holds one timestamp per element — read it like any other signal. |
|
||||
| `UDPS_TIME_FIRST_SAMPLE` / `UDPS_TIME_LAST_SAMPLE` | The signal at `time_signal_idx` is a scalar stamping element 0 (or N−1); the rest follow at `1/sampling_rate`. |
|
||||
|
||||
The time signal is a raw producer-side counter (µs, or ns when it is a `uint64`), not wall clock,
|
||||
so plotting it against real time needs a one-off calibration against `recv_time` — that is what
|
||||
the Go hub does. `udps_frame_element_time()` skips all that and returns an arrival-anchored
|
||||
estimate: good enough for a quick plot, but when a time signal exists, it is the accurate source.
|
||||
|
||||
---
|
||||
|
||||
## Diagnosing loss
|
||||
|
||||
```c
|
||||
udps_stats_t s;
|
||||
udps_client_stats(cli, &s);
|
||||
```
|
||||
|
||||
| Counter | Meaning |
|
||||
|---|---|
|
||||
| `packets_received`, `bytes_received` | Accepted datagrams and TCP frames. |
|
||||
| `frames_delivered` | DATA packets decoded and passed to `on_data`. |
|
||||
| `config_updates` | CONFIG packets applied. |
|
||||
| `counter_gaps` | Missing packet counters — datagrams lost on the wire or in the kernel. |
|
||||
| `fragments_dropped` | Duplicate, stale or unplaceable fragments; a non-zero value with `counter_gaps` means fragmented updates are arriving incomplete. |
|
||||
| `reconnects` | Sessions re-established after a silence timeout. |
|
||||
|
||||
Persistent loss on a fast stream is almost always the receive buffer: raise `recv_buffer_bytes`
|
||||
(and `net.core.rmem_max`, which caps it). A fragmented producer is more fragile than one sending
|
||||
whole cycles, because losing any fragment discards the whole update — if you control the
|
||||
producer, sizing `MaxPayloadSize` above one cycle removes that failure mode entirely.
|
||||
|
||||
---
|
||||
|
||||
## Example program
|
||||
|
||||
`example/udps_dump.c` connects, prints the signal table on CONFIG, then a throttled summary of
|
||||
each frame, and a receive-statistics report on Ctrl-C.
|
||||
|
||||
```bash
|
||||
# unicast
|
||||
./udps_dump --host 127.0.0.1 --port 44500
|
||||
|
||||
# multicast
|
||||
./udps_dump --host 127.0.0.1 --port 44500 --multicast 239.0.0.1 --iface 127.0.0.1
|
||||
|
||||
# quieter, and stop after 500 frames
|
||||
./udps_dump --host 127.0.0.1 --port 44500 --interval 5 --frames 500
|
||||
```
|
||||
|
||||
| Flag | Meaning |
|
||||
|---|---|
|
||||
| `--host ADDR` | Server address (default `127.0.0.1`). |
|
||||
| `--port N` | Server UDP port, or TCP control port in multicast mode (default 44500). |
|
||||
| `--multicast GROUP` | Join `GROUP` for data instead of using unicast. |
|
||||
| `--iface ADDR` | Local interface address for the multicast join. |
|
||||
| `--data-port N` | Multicast data port (default `--port + 1`). |
|
||||
| `--silence SEC` | Reconnect after `SEC` without data; `0` disables. |
|
||||
| `--interval SEC` | Seconds between printouts (default 1). |
|
||||
| `--frames N` | Exit after `N` frames. |
|
||||
|
||||
Against the repository's own producer (`./run_udp_producer.sh -n 2`, two 1 Msps channels of
|
||||
1000-element `float32` arrays at 1 kHz) the output looks like:
|
||||
|
||||
```
|
||||
CONFIG: 3 signal(s), publish mode strict
|
||||
# name type shape unit rate[Hz] time-mode
|
||||
0 TimeArray uint64 1x1000 ns 0 packet
|
||||
1 Ch1 float32 1x1000 V 0 full-array
|
||||
2 Ch2 float32 1x1000 V 0 full-array
|
||||
|
||||
frame #2289897 t=1787409851.723466 samples=1
|
||||
Ch1 n=1000 first=-6.9e-10 last=-0.00628 min=-1 max=1 V
|
||||
Ch2 n=1000 first=0.5 last=0.49975 min=-0.5 max=0.5 V
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Limitations
|
||||
|
||||
- IPv4 only, matching the protocol and the producer.
|
||||
- One thread per client; there is no internal locking.
|
||||
- The receive path allocates only when a CONFIG grows the signal set or a frame grows the decode
|
||||
arena, so a steady stream is allocation-free — but this is not a hard real-time component.
|
||||
- DATA arriving before the first CONFIG is dropped: without descriptors it cannot be decoded.
|
||||
This is normal for a few packets after joining a multicast group.
|
||||
@@ -244,6 +244,7 @@ Open `http://localhost:9090`, explore the object tree, trace signals, force valu
|
||||
| ----------------------------- | -------------------------------------------------------------- |
|
||||
| `Docs/Protocol.md` | UDPS binary wire protocol specification |
|
||||
| `Docs/UDPStreamer.md` | UDPStreamer DataSource configuration reference |
|
||||
| `Docs/UDPS-C-Client.md` | Standalone C/C++ UDPS receiver library (`Common/Client/c`) |
|
||||
| `Docs/SineArrayGAM.md` | SineArrayGAM configuration reference |
|
||||
| `Docs/DebugService.md` | DebugService TCP API and architecture |
|
||||
| `Docs/Tutorial.md` | Step-by-step tutorial covering both components |
|
||||
|
||||
Reference in New Issue
Block a user