FERS 0.1.0
The Flexible Extensible Radar Simulator
Loading...
Searching...
No Matches
vita49StreamingStore.test.ts
Go to the documentation of this file.
1// SPDX-License-Identifier: GPL-2.0-only
2// Copyright (c) 2026-present FERS Contributors (see AUTHORS.md).
3
4import { describe, expect, test } from 'bun:test';
5import { defaultGlobalParameters } from './scenarioStore/defaults';
6import type { ScenarioData } from './scenarioStore/types';
7import {
8 DEFAULT_VITA49_CONFIG,
9 deriveExpectedVita49Streams,
10 mergeVita49StreamRows,
11 toVita49BackendConfig,
12 useVita49StreamingStore,
13 type Vita49PacketTraceEvent,
14 validateVita49Config,
15} from './vita49StreamingStore';
16
17const packet = (sequence: number): Vita49PacketTraceEvent => ({
18 sequence,
19 event: 'data',
20 stream_id: 10,
21 byte_count: 128,
22 sample_count: 24,
23 first_sample_time: 0,
24 timestamp: { integer_seconds: 1_700_000_000, fractional_picoseconds: 0 },
25 data_packet: true,
26 context_packet: false,
27 dropped: false,
28 over_range: false,
29 sample_loss: false,
30});
31
32describe('VITA49 streaming config', () => {
33 test('validates malformed endpoint and runtime bounds', () => {
34 expect(
35 validateVita49Config({
36 ...DEFAULT_VITA49_CONFIG,
37 host: ' ',
38 port: 0,
39 fullscale: 0,
40 maxUdpPayload: 63,
41 queueDepth: 0,
42 packetTraceRingSize: 0,
43 })
44 ).toEqual([
45 'Host is required.',
46 'Port must be an integer from 1 to 65535.',
47 'Full-scale must be a positive finite number.',
48 'Max UDP payload must be an integer from 64 to 65507.',
49 'Queue depth must be an integer from 1 to 4294967295.',
50 'Packet trace ring size must be a positive integer.',
51 ]);
52 });
53
54 test('rejects queue depth above uint32 max', () => {
55 expect(
56 validateVita49Config({
57 ...DEFAULT_VITA49_CONFIG,
58 queueDepth: 4_294_967_296,
59 })
60 ).toContain('Queue depth must be an integer from 1 to 4294967295.');
61 expect(
62 validateVita49Config({
63 ...DEFAULT_VITA49_CONFIG,
64 queueDepth: 4_294_967_295,
65 })
66 ).not.toContain('Queue depth must be an integer from 1 to 4294967295.');
67 });
68
69 test('validates fixed epoch and preserves ns as a string for Rust', () => {
70 expect(
71 validateVita49Config({
72 ...DEFAULT_VITA49_CONFIG,
73 epochMode: 'fixed',
74 epochUnixNanoseconds: '4294967296000000000',
75 })
76 ).toContain('Fixed epoch must fit the VRT 32-bit UTC seconds field.');
77
78 expect(
79 toVita49BackendConfig({
80 ...DEFAULT_VITA49_CONFIG,
81 epochMode: 'fixed',
82 epochUnixNanoseconds: '1700000000123456789',
83 })
84 ).toMatchObject({
85 epoch_unix_nanoseconds: '1700000000123456789',
86 trace_enabled: true,
87 packet_trace_ring_size: DEFAULT_VITA49_CONFIG.packetTraceRingSize,
88 });
89 });
90});
91
92describe('VITA49 expected stream derivation', () => {
93 test('covers pulsed, CW, and FMCW receiver streams', () => {
94 const scenario: Pick<
95 ScenarioData,
96 'globalParameters' | 'platforms' | 'waveforms'
97 > = {
98 globalParameters: { ...defaultGlobalParameters, rate: 2e6 },
99 waveforms: [
100 {
101 id: '100',
102 type: 'Waveform',
103 name: 'CW',
104 waveformType: 'cw',
105 power: 1,
106 carrier_frequency: 9.6e9,
107 },
108 ],
109 platforms: [
110 {
111 id: '1',
112 type: 'Platform',
113 name: 'Platform A',
114 motionPath: {
115 interpolation: 'static',
116 waypoints: [
117 { id: '1', x: 0, y: 0, altitude: 0, time: 0 },
118 ],
119 },
120 rotation: {
121 type: 'fixed',
122 startAzimuth: 0,
123 startElevation: 0,
124 azimuthRate: 0,
125 elevationRate: 0,
126 },
127 components: [
128 {
129 id: '10',
130 type: 'receiver',
131 name: 'Rx',
132 radarType: 'pulsed',
133 window_skip: 1e-6,
134 window_length: 10e-6,
135 prf: 1000,
136 antennaId: null,
137 timingId: null,
138 noiseTemperature: null,
139 noDirectPaths: false,
140 noPropagationLoss: false,
141 schedule: [],
142 },
143 {
144 id: '11',
145 type: 'receiver',
146 name: 'CwRx',
147 radarType: 'cw',
148 window_skip: null,
149 window_length: null,
150 prf: null,
151 antennaId: null,
152 timingId: null,
153 noiseTemperature: null,
154 noDirectPaths: false,
155 noPropagationLoss: false,
156 schedule: [],
157 },
158 {
159 id: '12',
160 type: 'monostatic',
161 name: 'Mono',
162 txId: '13',
163 rxId: '14',
164 radarType: 'fmcw',
165 window_skip: null,
166 window_length: null,
167 prf: null,
168 antennaId: null,
169 waveformId: '100',
170 timingId: null,
171 noiseTemperature: null,
172 noDirectPaths: false,
173 noPropagationLoss: false,
174 fmcwModeConfig: { if_sample_rate: 250000 },
175 schedule: [],
176 },
177 ],
178 },
179 ],
180 };
181
182 expect(deriveExpectedVita49Streams(scenario)).toMatchObject([
183 {
184 receiverId: 10,
185 receiverName: 'Rx',
186 mode: 'pulsed',
187 sampleRate: 2e6,
188 referenceFrequency: null,
189 },
190 {
191 receiverId: 11,
192 receiverName: 'CwRx',
193 mode: 'cw',
194 sampleRate: 2e6,
195 referenceFrequency: null,
196 },
197 {
198 receiverId: 14,
199 receiverName: 'Mono',
200 mode: 'fmcw',
201 sampleRate: 250000,
202 referenceFrequency: 9.6e9,
203 },
204 ]);
205 });
206});
207
208describe('VITA49 run lifecycle', () => {
209 test('tracks draining between active streaming and completion', () => {
210 useVita49StreamingStore.setState({
211 runState: 'idle',
212 expectedStreams: [],
213 streamStats: null,
214 packetTrace: [],
215 omittedPacketTraceEvents: 0,
216 finalMetadata: null,
217 finalVita49Metadata: null,
218 error: null,
219 });
220
221 useVita49StreamingStore.getState().startRun([]);
222 expect(useVita49StreamingStore.getState().runState).toBe('running');
223
224 useVita49StreamingStore.getState().markDraining();
225 expect(useVita49StreamingStore.getState().runState).toBe('draining');
226
227 useVita49StreamingStore.getState().completeRun(null);
228 expect(useVita49StreamingStore.getState().runState).toBe('completed');
229 });
230});
231
232describe('VITA49 telemetry rows', () => {
233 test('maps exact VRT timestamp objects and simulation span', () => {
234 const rows = mergeVita49StreamRows([], {
235 mode: 'vita49_udp',
236 epoch_unix_nanoseconds: '1700000000123456789',
237 streams: [
238 {
239 receiver_id: 7,
240 receiver_name: 'Rx',
241 stream_id: 1234,
242 mode: 'pulsed',
243 sample_rate: 100000,
244 reference_frequency: 10000000,
245 packets_emitted: 2930,
246 samples_emitted: 1000000,
247 packets_dropped: 0,
248 samples_dropped: 0,
249 over_range_count: 0,
250 late_packet_count: 0,
251 context_packets: 12,
252 first_sample_time: 0,
253 end_sample_time: 10,
254 first_timestamp: {
255 integer_seconds: 1700000000,
256 fractional_picoseconds: 123456789000,
257 },
258 end_timestamp: {
259 integer_seconds: 1700000010,
260 fractional_picoseconds: 123456789000,
261 },
262 },
263 ],
264 });
265
266 expect(rows).toMatchObject([
267 {
268 receiverId: 7,
269 mode: 'pulsed',
270 samplesEmitted: 1000000,
271 firstSampleTime: 0,
272 endSampleTime: 10,
273 firstTimestamp: {
274 integer_seconds: 1700000000,
275 fractional_picoseconds: 123456789000,
276 },
277 endTimestamp: {
278 integer_seconds: 1700000010,
279 fractional_picoseconds: 123456789000,
280 },
281 },
282 ]);
283 });
284
285 test('does not default backend-only streams to CW', () => {
286 const rows = mergeVita49StreamRows([], {
287 mode: 'vita49_udp',
288 epoch_unix_nanoseconds: null,
289 streams: [
290 {
291 receiver_id: 8,
292 receiver_name: 'BackendRx',
293 stream_id: 5678,
294 sample_rate: 100000,
295 reference_frequency: 10000000,
296 packets_emitted: 1,
297 samples_emitted: 10,
298 packets_dropped: 0,
299 samples_dropped: 0,
300 over_range_count: 0,
301 late_packet_count: 0,
302 context_packets: 2,
303 first_sample_time: null,
304 end_sample_time: null,
305 first_timestamp: null,
306 end_timestamp: null,
307 },
308 ],
309 });
310
311 expect(rows).toMatchObject([
312 {
313 receiverId: 8,
314 mode: 'unknown',
315 backendObserved: true,
316 },
317 ]);
318 });
319});
320
321describe('VITA49 packet trace ring', () => {
322 test('bounds packets and counts omitted events', () => {
323 useVita49StreamingStore.setState({
324 config: { ...DEFAULT_VITA49_CONFIG, packetTraceRingSize: 3 },
325 packetTrace: [],
326 omittedPacketTraceEvents: 0,
327 });
328
329 useVita49StreamingStore
330 .getState()
331 .appendPacketBatch([1, 2, 3, 4, 5].map(packet));
332
333 const state = useVita49StreamingStore.getState();
334 expect(state.packetTrace.map((entry) => entry.sequence)).toEqual([
335 3, 4, 5,
336 ]);
337 expect(state.omittedPacketTraceEvents).toBe(2);
338 });
339
340 test('adds backend omitted packet count when appending polled batches', () => {
341 useVita49StreamingStore.setState({
342 config: { ...DEFAULT_VITA49_CONFIG, packetTraceRingSize: 5 },
343 packetTrace: [],
344 omittedPacketTraceEvents: 0,
345 });
346
347 useVita49StreamingStore
348 .getState()
349 .appendPacketBatch([1, 2].map(packet), 7);
350
351 const state = useVita49StreamingStore.getState();
352 expect(state.packetTrace.map((entry) => entry.sequence)).toEqual([
353 1, 2,
354 ]);
355 expect(state.omittedPacketTraceEvents).toBe(7);
356 });
357});