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_data_packet_count: 3,
251 late_context_packet_count: 9,
252 context_packets: 12,
253 first_sample_time: 0,
254 end_sample_time: 10,
255 first_timestamp: {
256 integer_seconds: 1700000000,
257 fractional_picoseconds: 123456789000,
258 },
259 end_timestamp: {
260 integer_seconds: 1700000010,
261 fractional_picoseconds: 123456789000,
262 },
263 },
264 ],
265 });
266
267 expect(rows).toMatchObject([
268 {
269 receiverId: 7,
270 mode: 'pulsed',
271 samplesEmitted: 1000000,
272 lateDataPacketCount: 3,
273 lateContextPacketCount: 9,
274 firstSampleTime: 0,
275 endSampleTime: 10,
276 firstTimestamp: {
277 integer_seconds: 1700000000,
278 fractional_picoseconds: 123456789000,
279 },
280 endTimestamp: {
281 integer_seconds: 1700000010,
282 fractional_picoseconds: 123456789000,
283 },
284 },
285 ]);
286 });
287
288 test('does not default backend-only streams to CW', () => {
289 const rows = mergeVita49StreamRows([], {
290 mode: 'vita49_udp',
291 epoch_unix_nanoseconds: null,
292 streams: [
293 {
294 receiver_id: 8,
295 receiver_name: 'BackendRx',
296 stream_id: 5678,
297 sample_rate: 100000,
298 reference_frequency: 10000000,
299 packets_emitted: 1,
300 samples_emitted: 10,
301 packets_dropped: 0,
302 samples_dropped: 0,
303 over_range_count: 0,
304 late_data_packet_count: 0,
305 late_context_packet_count: 0,
306 context_packets: 2,
307 first_sample_time: null,
308 end_sample_time: null,
309 first_timestamp: null,
310 end_timestamp: null,
311 },
312 ],
313 });
314
315 expect(rows).toMatchObject([
316 {
317 receiverId: 8,
318 mode: 'unknown',
319 backendObserved: true,
320 },
321 ]);
322 });
323});
324
325describe('VITA49 packet trace ring', () => {
326 test('bounds packets and counts omitted events', () => {
327 useVita49StreamingStore.setState({
328 config: { ...DEFAULT_VITA49_CONFIG, packetTraceRingSize: 3 },
329 packetTrace: [],
330 omittedPacketTraceEvents: 0,
331 });
332
333 useVita49StreamingStore
334 .getState()
335 .appendPacketBatch([1, 2, 3, 4, 5].map(packet));
336
337 const state = useVita49StreamingStore.getState();
338 expect(state.packetTrace.map((entry) => entry.sequence)).toEqual([
339 3, 4, 5,
340 ]);
341 expect(state.omittedPacketTraceEvents).toBe(2);
342 });
343
344 test('adds backend omitted packet count when appending polled batches', () => {
345 useVita49StreamingStore.setState({
346 config: { ...DEFAULT_VITA49_CONFIG, packetTraceRingSize: 5 },
347 packetTrace: [],
348 omittedPacketTraceEvents: 0,
349 });
350
351 useVita49StreamingStore
352 .getState()
353 .appendPacketBatch([1, 2].map(packet), 7);
354
355 const state = useVita49StreamingStore.getState();
356 expect(state.packetTrace.map((entry) => entry.sequence)).toEqual([
357 1, 2,
358 ]);
359 expect(state.omittedPacketTraceEvents).toBe(7);
360 });
361});