#!/usr/bin/env python3 # SPDX-License-Identifier: MIT # SPDX-FileCopyrightText: 2026 Martino Ferrari """A minimal UDPStreamer (UDPS) producer, for exercising udpscope without MARTe2. Unicast only: binds `--port`, waits for a CONNECT datagram, then streams CONFIG once and DATA packets to whoever connected. Publish mode is Accumulate, so each packet carries `--batch` consecutive samples per (scalar) signal -- the shape the viewer's per-sample path expects. Signals: `time` (the stream's own clock, seconds), `sine`, `square`, `ramp`. Everything is float64 and unquantised. ./tools/mock_udpstreamer.py --port 8000 --rate 10000 --batch 100 """ import argparse import math import random import socket import struct import time 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, TIME_FIRST_SAMPLE = 0, 1, 2 PUBLISH_ACCUMULATE = 1 NO_TIME_SIGNAL = 0xFFFFFFFF def header(pkt_type, counter, payload_bytes): return struct.pack(" 0.0 else stamps # The interferer is a tone at 100x the fundamental with a randomly scaled amplitude, which # is the shape that is hardest on anything counting crossings: near the sine's own mid level # the fundamental's slope is 2*pi*f while the interferer's is 100x that, so the signal # recrosses the level several times per true crossing. `--noise 0` is the ideal waveform, and # it is the default because the documented probe assertions are *exact* (`peak to peak=2`, # `top=0.636620`) and any interferer moves the extremes. if noise > 0.0: sine = [ math.sin(2.0 * math.pi * freq * t) + random.random() * noise * math.sin(2.0 * math.pi * 100.0 * freq * t) for t in stamps ] else: 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 = ( [wire_stamps[0]] * batch if time_mode == TIME_FIRST_SAMPLE else wire_stamps ) payload = struct.pack("= 17: magic, ptype = struct.unpack_from(" 0.0: pdt = dt * (1.0 + random.uniform(-args.clock_jitter, args.clock_jitter)) spike = 0.0 if args.time_glitch_at >= 0.0 and not glitched and t_stream >= args.time_glitch_at: spike = args.time_glitch_by glitched = True print( "mock udpstreamer: time-signal glitch at t=%.3f, +%.3f s for one packet" % (t_stream, spike), flush=True, ) sock.sendto( build_data( counter, t_stream, pdt, args.batch, args.freq, time_mode, args.noise, spike, ), peer, ) counter += 1 t_stream += args.batch * pdt next_send += period if ( args.duration > 0.0 and started is not None and now - started > args.duration ): print("mock udpstreamer: done (%d packets)" % counter, flush=True) return if __name__ == "__main__": main()