fix: time a FIRST_SAMPLE stream by its cadence, not by a second stamp
A UDPS_TIME_FIRST_SAMPLE time signal carries one meaningful value -- the
batch's first sample -- but a scalar signal in Accumulate mode still occupies
num_samples slots on the wire, which a producer fills by repeating that
anchor. Deriving the step from value_of(n - 1) therefore came out as zero, so
every sample of a frame landed on one x, the degeneracy guard tripped, and the
whole stream was timed by packet arrival instead. On a 1 MS/s source that
arrival clock is not monotonic within a frame: the export's time column held
90559 order breaks.
Space the batch by the cadence, exactly as the LAST_SAMPLE branch beside it
already dates its own batch backwards. The export's time column is now
strictly increasing (0 duplicates, 0 backwards steps) and reads the declared
1 MS/s.
mock_udpstreamer gains --time-mode {full,first}; it could only emit
FULL_ARRAY, so nothing exercised this branch. In 'first' it repeats the anchor
across the batch's wire slots -- the shape that produced the defect -- while
still generating the waveforms from the real per-sample stamps.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
1fc2a3afe2
commit
eb4372242c
2 files changed
+36
-15
No files matched your search
+27
-13
@@ -25,7 +25,7 @@ MAGIC = 0x53504455
|
||||
PKT_DATA, PKT_CONFIG, PKT_ACK, PKT_CONNECT, PKT_DISCONNECT = 0, 1, 2, 3, 4
|
||||
T_FLOAT64 = 9
|
||||
QUANT_NONE = 0
|
||||
TIME_PACKET, TIME_FULL_ARRAY = 0, 1
|
||||
TIME_PACKET, TIME_FULL_ARRAY, TIME_FIRST_SAMPLE = 0, 1, 2
|
||||
PUBLISH_ACCUMULATE = 1
|
||||
NO_TIME_SIGNAL = 0xFFFFFFFF
|
||||
|
||||
@@ -50,28 +50,35 @@ def signal_desc(name, *, rate, time_mode, time_idx, unit, rows=1, cols=1, dims=0
|
||||
return bytes(d)
|
||||
|
||||
|
||||
def build_config(rate):
|
||||
def build_config(rate, time_mode=TIME_FULL_ARRAY):
|
||||
sigs = [
|
||||
# The stream's clock: referenced by every data signal, so udpscope treats
|
||||
# it as the time base rather than a plottable channel.
|
||||
signal_desc("time", rate=0.0, time_mode=TIME_FULL_ARRAY,
|
||||
# it as the time base rather than a plottable channel. Its *own* time_mode
|
||||
# is what udpscope switches on, so `--time-mode first` has to land here.
|
||||
signal_desc("time", rate=0.0, time_mode=time_mode,
|
||||
time_idx=NO_TIME_SIGNAL, unit="s"),
|
||||
signal_desc("sine", rate=rate, time_mode=TIME_FULL_ARRAY, time_idx=0, unit="V"),
|
||||
signal_desc("square", rate=rate, time_mode=TIME_FULL_ARRAY, time_idx=0, unit="V"),
|
||||
signal_desc("ramp", rate=rate, time_mode=TIME_FULL_ARRAY, time_idx=0, unit="V"),
|
||||
signal_desc("sine", rate=rate, time_mode=time_mode, time_idx=0, unit="V"),
|
||||
signal_desc("square", rate=rate, time_mode=time_mode, time_idx=0, unit="V"),
|
||||
signal_desc("ramp", rate=rate, time_mode=time_mode, time_idx=0, unit="V"),
|
||||
]
|
||||
payload = struct.pack("<I", len(sigs)) + b"".join(sigs)
|
||||
payload += bytes([PUBLISH_ACCUMULATE])
|
||||
return header(PKT_CONFIG, 0, len(payload)) + payload
|
||||
|
||||
|
||||
def build_data(counter, t0, dt, batch, freq):
|
||||
def build_data(counter, t0, dt, batch, freq, time_mode=TIME_FULL_ARRAY):
|
||||
stamps = [t0 + i * dt for i in range(batch)]
|
||||
sine = [math.sin(2.0 * math.pi * freq * t) for t in stamps]
|
||||
square = [1.0 if math.sin(2.0 * math.pi * freq * 0.5 * t) >= 0.0 else -1.0 for t in stamps]
|
||||
ramp = [(freq * t) % 1.0 * 2.0 - 1.0 for t in stamps]
|
||||
# In FIRST_SAMPLE mode only element 0 of the time signal is meaningful: the producer
|
||||
# stamps the first sample of the batch and says nothing about the rest. A scalar
|
||||
# signal in Accumulate mode still occupies `batch` slots on the wire (the client
|
||||
# reads `num_samples` values for any 1-element signal), so the anchor is repeated --
|
||||
# which is exactly the shape that made udpscope derive a step of zero.
|
||||
wire_time = [t0] * batch if time_mode == TIME_FIRST_SAMPLE else stamps
|
||||
payload = struct.pack("<QI", int(t0 * 1e9), batch)
|
||||
for arr in (stamps, sine, square, ramp):
|
||||
for arr in (wire_time, sine, square, ramp):
|
||||
payload += struct.pack("<%dd" % batch, *arr)
|
||||
return header(PKT_DATA, counter, len(payload)) + payload
|
||||
|
||||
@@ -88,6 +95,12 @@ def main():
|
||||
"clock advances ten times slower than the wire delivers samples")
|
||||
ap.add_argument("--declared-rate", type=float, default=-1.0,
|
||||
help="sampling_rate to put in CONFIG; -1 = the real rate, 0 = 'unknown'")
|
||||
ap.add_argument("--time-mode", choices=("full", "first"), default="full",
|
||||
help="how the stream carries its clock: 'full' stamps every sample "
|
||||
"(UDPS_TIME_FULL_ARRAY), 'first' stamps only the batch's first "
|
||||
"sample (UDPS_TIME_FIRST_SAMPLE) and leaves the viewer to space the "
|
||||
"rest by the declared cadence -- the shape a real MARTe2-style "
|
||||
"producer with a single u64 clock has")
|
||||
ap.add_argument("--clock-jitter", type=float, default=0.0,
|
||||
help="per-packet stamp jitter, as a fraction of one packet's span: a device "
|
||||
"that stamps each batch from its own clock does not land on a perfect "
|
||||
@@ -98,14 +111,15 @@ def main():
|
||||
dt = args.stream_scale / args.rate # stream seconds per sample
|
||||
period = args.batch / args.rate # wall seconds per packet
|
||||
declared = args.rate if args.declared_rate < 0.0 else args.declared_rate
|
||||
time_mode = TIME_FIRST_SAMPLE if args.time_mode == "first" else TIME_FULL_ARRAY
|
||||
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
sock.bind(("0.0.0.0", args.port))
|
||||
sock.settimeout(0.001)
|
||||
print("mock udpstreamer: waiting for CONNECT on :%d (%.0f S/s wire, %d per packet, "
|
||||
"stream clock x%.3g, declared %.0f S/s)"
|
||||
% (args.port, args.rate, args.batch, args.stream_scale, declared), flush=True)
|
||||
"stream clock x%.3g, declared %.0f S/s, time-mode %s)"
|
||||
% (args.port, args.rate, args.batch, args.stream_scale, declared, args.time_mode), flush=True)
|
||||
|
||||
peer = None
|
||||
counter = 1
|
||||
@@ -124,7 +138,7 @@ def main():
|
||||
t_stream = 0.0
|
||||
started = time.monotonic()
|
||||
next_send = started
|
||||
sock.sendto(build_config(declared), peer)
|
||||
sock.sendto(build_config(declared, time_mode), peer)
|
||||
print("mock udpstreamer: CONNECT from %s:%d, CONFIG sent" % peer, flush=True)
|
||||
elif magic == MAGIC and ptype == PKT_DISCONNECT:
|
||||
print("mock udpstreamer: DISCONNECT", flush=True)
|
||||
@@ -148,7 +162,7 @@ def main():
|
||||
pdt = dt
|
||||
if args.clock_jitter > 0.0:
|
||||
pdt = dt * (1.0 + random.uniform(-args.clock_jitter, args.clock_jitter))
|
||||
sock.sendto(build_data(counter, t_stream, pdt, args.batch, args.freq), peer)
|
||||
sock.sendto(build_data(counter, t_stream, pdt, args.batch, args.freq, time_mode), peer)
|
||||
counter += 1
|
||||
t_stream += args.batch * pdt
|
||||
next_send += period
|
||||
|
||||
Reference in new issue
Block a user