diff --git a/src/acquisition.cpp b/src/acquisition.cpp index e087323..d4451fc 100644 --- a/src/acquisition.cpp +++ b/src/acquisition.cpp @@ -183,9 +183,16 @@ std::uint32_t Acquisition::build_stamps(const udps_frame_t* frame, int time_sign used_stream_time = true; break; case UDPS_TIME_FIRST_SAMPLE: { + // Only element 0 is meaningful here: the producer stamps the batch's first + // sample and says nothing about the rest, so the cadence has to space them -- + // exactly as LAST_SAMPLE below dates its batch backwards from its anchor. + // Deriving a step from `value_of(n - 1)` instead cannot work: a scalar signal + // in Accumulate mode still occupies `num_samples` slots on the wire, and a + // producer fills them by repeating the anchor, so the step came out as zero + // and every sample of the frame landed on one x. The degeneracy guard below + // then threw the whole stream onto the arrival clock. const double anchor = value_of(0); - const double step = n > 1 ? (value_of(n - 1) - anchor) / double(n - 1) : period; - for (std::uint32_t k = 0; k < n; ++k) stamps_[k] = anchor + double(k) * step; + for (std::uint32_t k = 0; k < n; ++k) stamps_[k] = anchor + double(k) * cadence; used_stream_time = true; break; } diff --git a/tools/mock_udpstreamer.py b/tools/mock_udpstreamer.py index 685384e..10f7bd8 100644 --- a/tools/mock_udpstreamer.py +++ b/tools/mock_udpstreamer.py @@ -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("= 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(" 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