FERS 1.0.0
The Flexible Extensible Radar Simulator
Loading...
Searching...
No Matches
backendSlice.ts
Go to the documentation of this file.
1// SPDX-License-Identifier: GPL-2.0-only
2// Copyright (c) 2025-present FERS Contributors (see AUTHORS.md).
3
4import { StateCreator } from 'zustand';
5import { invoke } from '@tauri-apps/api/core';
6import {
7 ScenarioStore,
8 BackendActions,
9 TargetComponent,
10 Timing,
11} from '../types';
12import { omit } from '@/utils/typeUtils.ts';
13
14// Helper to strip null/undefined values from an object before sending to backend
15const cleanObject = <T>(obj: T): T => {
16 if (Array.isArray(obj)) {
17 return obj.map(cleanObject) as unknown as T;
18 }
19 if (obj !== null && typeof obj === 'object') {
20 const newObj = {} as Record<string, unknown>;
21 for (const [key, value] of Object.entries(obj)) {
22 if (value !== null && value !== undefined) {
23 newObj[key] = cleanObject(value);
24 }
25 }
26 return newObj as T;
27 }
28 return obj;
29};
30
31// --- Backend-specific type definitions ---
32type BackendTarget = {
33 name: string;
34 rcs: {
35 type: TargetComponent['rcs_type'];
36 value?: number;
37 filename?: string;
38 };
39 model?: {
40 type: Exclude<TargetComponent['rcs_model'], 'constant'>;
41 k?: number;
42 };
43};
44
45export const createBackendSlice: StateCreator<
46 ScenarioStore,
47 [['zustand/immer', never]],
48 [],
49 BackendActions
50> = (set, get) => ({
51 syncBackend: async () => {
52 set({ isBackendSyncing: true });
53 const { globalParameters, waveforms, timings, antennas, platforms } =
54 get();
55
56 // Helper functions to map frontend asset IDs back to names for the backend
57 const findAntennaName = (id: string | null) =>
58 antennas.find((a) => a.id === id)?.name;
59 const findWaveformName = (id: string | null) =>
60 waveforms.find((p) => p.id === id)?.name;
61 const findTimingName = (id: string | null) =>
62 timings.find((t) => t.id === id)?.name;
63
64 const backendPlatforms = platforms.map((p) => {
65 const { components, motionPath, rotation, ...rest } = p;
66
67 // Map the list of components to backend objects
68 const backendComponents = components.map((component) => {
69 let compObj = {};
70 const mode =
71 'radarType' in component && component.radarType === 'pulsed'
72 ? {
73 pulsed_mode: {
74 prf: component.prf,
75 ...(component.type !== 'transmitter' && {
76 window_skip: component.window_skip,
77 window_length: component.window_length,
78 }),
79 },
80 }
81 : 'radarType' in component &&
82 component.radarType === 'cw'
83 ? { cw_mode: {} }
84 : {};
85
86 switch (component.type) {
87 case 'monostatic':
88 compObj = {
89 monostatic: {
90 name: component.name,
91 ...mode,
92 antenna: findAntennaName(component.antennaId),
93 waveform: findWaveformName(
94 component.waveformId
95 ),
96 timing: findTimingName(component.timingId),
97 noise_temp: component.noiseTemperature,
98 nodirect: component.noDirectPaths,
99 nopropagationloss: component.noPropagationLoss,
100 schedule: component.schedule,
101 },
102 };
103 break;
104 case 'transmitter':
105 compObj = {
106 transmitter: {
107 name: component.name,
108 ...mode,
109 antenna: findAntennaName(component.antennaId),
110 waveform: findWaveformName(
111 component.waveformId
112 ),
113 timing: findTimingName(component.timingId),
114 schedule: component.schedule,
115 },
116 };
117 break;
118 case 'receiver':
119 compObj = {
120 receiver: {
121 name: component.name,
122 ...mode,
123 antenna: findAntennaName(component.antennaId),
124 timing: findTimingName(component.timingId),
125 noise_temp: component.noiseTemperature,
126 nodirect: component.noDirectPaths,
127 nopropagationloss: component.noPropagationLoss,
128 schedule: component.schedule,
129 },
130 };
131 break;
132 case 'target':
133 {
134 const targetObj: BackendTarget = {
135 name: component.name,
136 rcs: {
137 type: component.rcs_type,
138 value: component.rcs_value,
139 filename: component.rcs_filename,
140 },
141 };
142 if (component.rcs_model !== 'constant') {
143 targetObj.model = {
144 type: component.rcs_model,
145 k: component.rcs_k,
146 };
147 }
148 compObj = { target: targetObj };
149 }
150 break;
151 }
152 return cleanObject(compObj);
153 });
154
155 const backendRotation: Record<string, unknown> = {};
156 if (rotation.type === 'fixed') {
157 const r = omit(rotation, 'type');
158 backendRotation.fixedrotation = {
159 interpolation: 'constant',
160 startazimuth: r.startAzimuth,
161 startelevation: r.startElevation,
162 azimuthrate: r.azimuthRate,
163 elevationrate: r.elevationRate,
164 };
165 } else {
166 const r = omit(rotation, 'type');
167 backendRotation.rotationpath = {
168 interpolation: r.interpolation,
169 rotationwaypoints: r.waypoints.map((wp) => omit(wp, 'id')),
170 };
171 }
172
173 return cleanObject({
174 ...rest,
175 motionpath: {
176 interpolation: motionPath.interpolation,
177 positionwaypoints: motionPath.waypoints.map((wp) =>
178 omit(wp, 'id')
179 ),
180 },
181 ...backendRotation,
182 components: backendComponents,
183 });
184 });
185
186 const backendWaveforms = waveforms.map((w) => {
187 const waveformContent =
188 w.waveformType === 'cw'
189 ? { cw: {} }
190 : { pulsed_from_file: { filename: w.filename } };
191
192 return {
193 name: w.name,
194 power: w.power,
195 carrier_frequency: w.carrier_frequency,
196 ...waveformContent,
197 };
198 });
199
200 const backendTimings = timings.map((t: Timing) => {
201 const rest = omit(t, 'id', 'type');
202 const timingObj = {
203 ...rest,
204 synconpulse: false,
205 freq_offset: t.freqOffset,
206 random_freq_offset_stdev: t.randomFreqOffsetStdev,
207 phase_offset: t.phaseOffset,
208 random_phase_offset_stdev: t.randomPhaseOffsetStdev,
209 noise_entries: t.noiseEntries.map((entry) => omit(entry, 'id')),
210 };
211 // Remove noise_entries array if it's empty
212 if (timingObj.noise_entries?.length === 0) {
213 delete (timingObj as Partial<typeof timingObj>).noise_entries;
214 }
215 return timingObj;
216 });
217
218 const backendAntennas = antennas.map((a) =>
219 omit(a, 'id', 'type', 'meshScale')
220 );
221
222 const {
223 start,
224 end,
225 random_seed,
226 oversample_ratio,
227 coordinateSystem,
228 ...gpRest
229 } = globalParameters;
230
231 const gp_params = {
232 ...gpRest,
233 starttime: start,
234 endtime: end,
235 randomseed: random_seed,
236 oversample: oversample_ratio,
237 coordinatesystem: coordinateSystem,
238 };
239
240 const scenarioJson = {
241 simulation: {
242 name: globalParameters.simulation_name,
243 parameters: cleanObject(gp_params),
244 waveforms: cleanObject(backendWaveforms),
245 timings: cleanObject(backendTimings),
246 antennas: cleanObject(backendAntennas),
247 platforms: backendPlatforms,
248 },
249 };
250
251 try {
252 const jsonPayload = JSON.stringify(scenarioJson, null, 2);
253 await invoke('update_scenario_from_json', {
254 json: jsonPayload,
255 });
256 console.log('Successfully synced state to backend.');
257
258 set((state) => {
259 state.isBackendSyncing = false;
260 state.backendVersion += 1;
261 });
262 } catch (error) {
263 console.error('Failed to sync state to backend:', error);
264 set({ isBackendSyncing: false });
265 throw error;
266 }
267 },
268 fetchFromBackend: async () => {
269 try {
270 const jsonState = await invoke<string>('get_scenario_as_json');
271 const scenarioData = JSON.parse(jsonState);
272 get().loadScenario(scenarioData);
273 } catch (error) {
274 console.error('Failed to fetch state from backend:', error);
275 throw error;
276 }
277 },
278});