FERS 1.0.0
The Flexible Extensible Radar Simulator
Loading...
Searching...
No Matches
lib.rs
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
4//! # Tauri Application Library Entry Point
5//!
6//! This module provides the main entry point for the FERS Tauri desktop application.
7//! It bridges the Rust/Tauri frontend with the C++ `libfers` simulation engine via
8//! a Foreign Function Interface (FFI).
9//!
10//! ## Architecture Overview
11//!
12//! The application follows a three-layer architecture:
13//!
14//! 1. **Frontend (TypeScript/React)**: Provides the user interface for scenario
15//! editing and visualization.
16//! 2. **Middle Layer (Rust/Tauri)**: This module. Exposes Tauri commands that the
17//! frontend can invoke, and manages the simulation state via a thread-safe wrapper.
18//! 3. **Backend (C++ via FFI)**: The `libfers` library that performs the actual
19//! simulation computations, parsing, and serialization.
20//!
21//! ## Thread Safety
22//!
23//! The `FersContext` (which wraps the C++ simulation state) is protected by a `Mutex`
24//! and stored in Tauri's managed state. This ensures that concurrent calls from the
25//! UI are serialized, preventing data races on the non-thread-safe C++ object.
26//!
27//! ## Tauri Commands
28//!
29//! All functions annotated with `#[tauri::command]` are exposed to the frontend via
30//! Tauri's IPC mechanism. They can be invoked asynchronously from JavaScript/TypeScript.
31
32mod fers_api;
33
34use std::sync::Mutex;
35use tauri::{AppHandle, Emitter, Manager, State};
36
37/// Data structure for a single motion waypoint received from the UI.
38///
39/// Coordinates should be in the scenario's define frame (e.g. ENU).
40#[derive(serde::Serialize, serde::Deserialize)]
41pub struct MotionWaypoint {
42 /// Time in seconds.
43 time: f64,
44 /// Easting/X coordinate in meters.
45 x: f64,
46 /// Northing/Y coordinate in meters.
47 y: f64,
48 /// Altitude/Z coordinate in meters (MSL).
49 altitude: f64,
50}
51
52/// Enum for the interpolation type received from the UI.
53#[derive(serde::Serialize, serde::Deserialize)]
54#[serde(rename_all = "lowercase")]
55pub enum InterpolationType {
56 Static,
57 Linear,
58 Cubic,
59}
60
61/// Data structure for a single interpolated point sent back to the UI.
62///
63/// Represents the physical state of a platform at a specific time step.
64#[derive(serde::Serialize, serde::Deserialize)]
65pub struct InterpolatedMotionPoint {
66 /// X position in meters.
67 x: f64,
68 /// Y position in meters.
69 y: f64,
70 /// Z position in meters.
71 z: f64,
72 /// X velocity in m/s.
73 vx: f64,
74 /// Y velocity in m/s.
75 vy: f64,
76 /// Z velocity in m/s.
77 vz: f64,
78}
79
80/// Data structure for a single rotation waypoint received from the UI.
81#[derive(serde::Serialize, serde::Deserialize)]
82pub struct RotationWaypoint {
83 /// Time in seconds.
84 time: f64,
85 /// Azimuth in compass degrees (0=North, 90=East).
86 azimuth: f64,
87 /// Elevation in degrees (positive up).
88 elevation: f64,
89}
90
91/// Data structure for a single interpolated rotation point sent back to the UI.
92#[derive(serde::Serialize, serde::Deserialize)]
93pub struct InterpolatedRotationPoint {
94 /// Azimuth in compass degrees.
95 azimuth_deg: f64,
96 /// Elevation in degrees.
97 elevation_deg: f64,
98}
99
100/// Type alias for the managed Tauri state that holds the simulation context.
101///
102/// The `FersContext` is wrapped in a `Mutex` to ensure thread-safe access, as Tauri
103/// may invoke commands from multiple threads concurrently. This alias simplifies
104/// the function signatures of Tauri commands.
105type FersState = Mutex<fers_api::FersContext>;
106
107// --- Tauri Commands ---
108
109/// Loads a FERS scenario from an XML file into the simulation context.
110///
111/// This command replaces any existing in-memory scenario with the one parsed from
112/// the specified file. The file path is provided by the user via the frontend dialog.
113///
114/// # Parameters
115///
116/// * `filepath` - The absolute or relative path to the FERS XML scenario file.
117/// * `state` - Tauri-managed state containing the shared `FersContext`.
118///
119/// # Returns
120///
121/// * `Ok(())` if the scenario was successfully loaded.
122/// * `Err(String)` containing an error message if loading failed (e.g., file not found,
123/// invalid XML, or a Mutex lock error).
124///
125/// # Example (from frontend)
126///
127/// ```typescript
128/// import { invoke } from '@tauri-apps/api/core';
129/// await invoke('load_scenario_from_xml_file', { filepath: '/path/to/scenario.xml' });
130/// ```
131#[tauri::command]
132fn load_scenario_from_xml_file(
133 filepath: String,
134 state: State<'_, FersState>,
135) -> Result<(), String> {
136 state.lock().map_err(|e| e.to_string())?.load_scenario_from_xml_file(&filepath)
137}
138
139/// Retrieves the current in-memory scenario as a JSON string.
140///
141/// This command serializes the simulation state into JSON format, allowing the
142/// frontend to display and edit the scenario. The JSON structure mirrors the
143/// internal representation used by `libfers`.
144///
145/// # Parameters
146///
147/// * `state` - Tauri-managed state containing the shared `FersContext`.
148///
149/// # Returns
150///
151/// * `Ok(String)` containing the JSON representation of the scenario.
152/// * `Err(String)` containing an error message if serialization failed or if the
153/// Mutex could not be locked.
154///
155/// # Example (from frontend)
156///
157/// ```typescript
158/// import { invoke } from '@tauri-apps/api/core';
159/// const scenarioJson = await invoke<string>('get_scenario_as_json');
160/// const scenario = JSON.parse(scenarioJson);
161/// ```
162#[tauri::command]
163fn get_scenario_as_json(state: State<'_, FersState>) -> Result<String, String> {
164 state.lock().map_err(|e| e.to_string())?.get_scenario_as_json()
165}
166
167/// Retrieves the current in-memory scenario as a FERS XML string.
168///
169/// This command is typically used when the user wants to export the scenario
170/// (potentially modified in the UI) back to the standard FERS XML format for
171/// sharing or archival.
172///
173/// # Parameters
174///
175/// * `state` - Tauri-managed state containing the shared `FersContext`.
176///
177/// # Returns
178///
179/// * `Ok(String)` containing the XML representation of the scenario.
180/// * `Err(String)` containing an error message if serialization failed or if the
181/// Mutex could not be locked.
182///
183/// # Example (from frontend)
184///
185/// ```typescript
186/// import { invoke } from '@tauri-apps/api/core';
187/// const scenarioXml = await invoke<string>('get_scenario_as_xml');
188/// // Save scenarioXml to a file using Tauri's fs plugin
189/// ```
190#[tauri::command]
191fn get_scenario_as_xml(state: State<'_, FersState>) -> Result<String, String> {
192 state.lock().map_err(|e| e.to_string())?.get_scenario_as_xml()
193}
194
195/// Updates the in-memory scenario from a JSON string provided by the frontend.
196///
197/// This is the primary method for applying changes made in the UI back to the
198/// simulation engine. The JSON is deserialized and used to rebuild the internal
199/// C++ world representation.
200///
201/// # Parameters
202///
203/// * `json` - A JSON string representing the modified scenario structure.
204/// * `state` - Tauri-managed state containing the shared `FersContext`.
205///
206/// # Returns
207///
208/// * `Ok(())` if the scenario was successfully updated.
209/// * `Err(String)` containing an error message if deserialization failed, the JSON
210/// structure was invalid, or the Mutex could not be locked.
211///
212/// # Example (from frontend)
213///
214/// ```typescript
215/// import { invoke } from '@tauri-apps/api/core';
216/// const updatedScenario = { /* modified scenario object */ };
217/// await invoke('update_scenario_from_json', { json: JSON.stringify(updatedScenario) });
218/// ```
219#[tauri::command]
220fn update_scenario_from_json(json: String, state: State<'_, FersState>) -> Result<(), String> {
221 state.lock().map_err(|e| e.to_string())?.update_scenario_from_json(&json)
222}
223
224/// Triggers the simulation based on the current in-memory scenario.
225///
226/// This command immediately returns `Ok(())` and spawns a background thread to
227/// perform the actual computationally intensive simulation. This prevents the UI
228/// from freezing. The result of the simulation (success or failure) is
229/// communicated back to the frontend via Tauri events.
230///
231/// # Parameters
232///
233/// * `app_handle` - The Tauri application handle, used to access managed state
234/// and emit events.
235///
236/// # Events Emitted
237///
238/// * `simulation-complete` - Emitted with `()` as payload on successful completion.
239/// * `simulation-error` - Emitted with a `String` error message on failure.
240/// * `simulation-progress` - Emitted periodically with `{ message: String, current: i32, total: i32 }`.
241#[tauri::command]
242fn run_simulation(app_handle: AppHandle) -> Result<(), String> {
243 // Clone the AppHandle so we can move it into the background thread.
244 let app_handle_clone = app_handle.clone();
245
246 // Spawn a new thread to run the blocking C++ simulation.
247 std::thread::spawn(move || {
248 // Retrieve the managed state within the new thread.
249 let fers_state: State<'_, FersState> = app_handle_clone.state();
250 let result = fers_state
251 .lock()
252 .map_err(|e| e.to_string())
253 .and_then(|context| context.run_simulation(&app_handle_clone));
254
255 // Emit an event to the frontend based on the simulation result.
256 match result {
257 Ok(_) => {
258 app_handle_clone
259 .emit("simulation-complete", ())
260 .expect("Failed to emit simulation-complete event");
261 }
262 Err(e) => {
263 app_handle_clone
264 .emit("simulation-error", e)
265 .expect("Failed to emit simulation-error event");
266 }
267 }
268 });
269
270 // Return immediately, allowing the UI to remain responsive.
271 Ok(())
272}
273
274/// Generates a KML visualization file for the current in-memory scenario.
275///
276/// This command spawns a background thread to handle file I/O and KML generation,
277/// preventing the UI from freezing. The result is communicated via events.
278///
279/// # Parameters
280///
281/// * `output_path` - The absolute file path where the KML file should be saved.
282/// * `app_handle` - The Tauri application handle.
283///
284/// # Events Emitted
285///
286/// * `kml-generation-complete` - Emitted with the output path `String` on success.
287/// * `kml-generation-error` - Emitted with a `String` error message on failure.
288#[tauri::command]
289fn generate_kml(output_path: String, app_handle: AppHandle) -> Result<(), String> {
290 let app_handle_clone = app_handle.clone();
291 std::thread::spawn(move || {
292 let fers_state: State<'_, FersState> = app_handle_clone.state();
293 let result = fers_state
294 .lock()
295 .map_err(|e| e.to_string())
296 .and_then(|context| context.generate_kml(&output_path));
297
298 match result {
299 Ok(_) => {
300 app_handle_clone
301 .emit("kml-generation-complete", &output_path)
302 .expect("Failed to emit kml-generation-complete event");
303 }
304 Err(e) => {
305 app_handle_clone
306 .emit("kml-generation-error", e)
307 .expect("Failed to emit kml-generation-error event");
308 }
309 }
310 });
311 Ok(())
312}
313
314/// A stateless command to calculate an interpolated motion path.
315///
316/// This command delegates to the `libfers` core to calculate a path from a given
317/// set of waypoints, ensuring the UI visualization is identical to the path
318/// the simulation would use.
319///
320/// # Parameters
321/// * `waypoints` - A vector of motion waypoints.
322/// * `interp_type` - The interpolation algorithm to use ('static', 'linear', 'cubic').
323/// * `num_points` - The desired number of points for the final path.
324///
325/// # Returns
326/// * `Ok(Vec<InterpolatedPoint>)` - The calculated path points.
327/// * `Err(String)` - An error message if the path calculation failed.
328#[tauri::command]
329fn get_interpolated_motion_path(
330 waypoints: Vec<MotionWaypoint>,
331 interp_type: InterpolationType,
332 num_points: usize,
333) -> Result<Vec<InterpolatedMotionPoint>, String> {
334 fers_api::get_interpolated_motion_path(waypoints, interp_type, num_points)
335}
336
337/// A stateless command to calculate an interpolated rotation path.
338///
339/// This command delegates to the `libfers` core to calculate a rotation path from a given
340/// set of waypoints. It is used by the UI to preview how the simulation will interpolate
341/// orientation changes over time.
342///
343/// # Parameters
344/// * `waypoints` - A vector of rotation waypoints (azimuth/elevation in compass degrees).
345/// * `interp_type` - The interpolation algorithm to use ('static', 'linear', 'cubic').
346/// * `num_points` - The desired number of points for the final path.
347///
348/// # Returns
349/// * `Ok(Vec<InterpolatedRotationPoint>)` - The calculated rotation points.
350/// * `Err(String)` - An error message if the calculation failed.
351#[tauri::command]
352fn get_interpolated_rotation_path(
353 waypoints: Vec<RotationWaypoint>,
354 interp_type: InterpolationType,
355 num_points: usize,
356) -> Result<Vec<InterpolatedRotationPoint>, String> {
357 fers_api::get_interpolated_rotation_path(waypoints, interp_type, num_points)
358}
359
360/// Retrieves a 2D antenna gain pattern for visualization.
361///
362/// This command samples the antenna model loaded in the current simulation context.
363/// It is stateful because it relies on the antenna assets defined in the loaded scenario.
364///
365/// # Parameters
366/// * `antenna_name` - The unique name of the antenna asset to sample.
367/// * `az_samples` - Resolution along the azimuth axis (e.g., 360).
368/// * `el_samples` - Resolution along the elevation axis (e.g., 180).
369/// * `frequency` - The frequency in Hz at which to calculate gain (relevant for frequency-dependent antennas).
370/// * `state` - The shared simulation state.
371///
372/// # Returns
373/// * `Ok(AntennaPatternData)` - Struct containing flattened gain array and dimensions.
374/// * `Err(String)` - Error if antenna not found or context locked.
375#[tauri::command]
376fn get_antenna_pattern(
377 antenna_name: String,
378 az_samples: usize,
379 el_samples: usize,
380 frequency: f64,
381 state: State<'_, FersState>,
382) -> Result<fers_api::AntennaPatternData, String> {
383 state.lock().map_err(|e| e.to_string())?.get_antenna_pattern(
384 &antenna_name,
385 az_samples,
386 el_samples,
387 frequency,
388 )
389}
390
391/// Calculates visual radio links between platforms at a specific time.
392///
393/// This command performs a lightweight geometric and physics check to determine
394/// which platforms can "see" each other. It distinguishes between monostatic,
395/// bistatic, and interference paths based on signal-to-noise ratios.
396///
397/// # Parameters
398/// * `time` - The simulation time in seconds to evaluate.
399/// * `state` - The shared simulation state containing platforms and physics models.
400///
401/// # Returns
402/// * `Ok(Vec<VisualLink>)` - A list of renderable link segments with metadata (type, quality, label).
403/// * `Err(String)` - Error if context access fails.
404#[tauri::command]
405fn get_preview_links(
406 time: f64,
407 state: State<'_, FersState>,
408) -> Result<Vec<fers_api::VisualLink>, String> {
409 state.lock().map_err(|e| e.to_string())?.calculate_preview_links(time)
410}
411
412/// Initializes and runs the Tauri application.
413///
414/// This function is the main entry point for the desktop application. It performs
415/// the following setup steps:
416///
417/// 1. Creates a new `FersContext` by calling the FFI layer. If this fails, it
418/// indicates a linking or initialization problem with `libfers`.
419/// 2. Registers Tauri plugins for file dialogs, file system access, and shell operations.
420/// 3. Stores the `FersContext` in Tauri's managed state, protected by a `Mutex`.
421/// 4. Registers all Tauri commands so they can be invoked from the frontend.
422/// 5. Launches the Tauri application event loop.
423///
424/// # Panics
425///
426/// This function will panic if:
427/// * The `FersContext` cannot be created (indicating a problem with `libfers`).
428/// * The Tauri application fails to start due to misconfiguration.
429///
430/// # Example
431///
432/// This function is typically called from `main.rs`:
433///
434/// ```rust
435/// fers_ui_lib::run();
436/// ```
437#[cfg_attr(mobile, tauri::mobile_entry_point)]
438pub fn run() {
439 // Attempt to create the FFI context. This validates that libfers is correctly linked.
440 let context = fers_api::FersContext::new()
441 .expect("Failed to create FERS context. Is libfers linked correctly?");
442
443 tauri::Builder::default()
444 // Register Tauri plugins for UI functionality
445 .plugin(tauri_plugin_dialog::init())
446 .plugin(tauri_plugin_opener::init())
447 .plugin(tauri_plugin_fs::init())
448 // Store the FersContext as managed state, accessible from all commands
449 .manage(Mutex::new(context))
450 // Register all Tauri commands that can be invoked from the frontend
451 .invoke_handler(tauri::generate_handler![
452 load_scenario_from_xml_file,
453 get_scenario_as_json,
454 get_scenario_as_xml,
455 update_scenario_from_json,
456 run_simulation,
457 generate_kml,
458 get_interpolated_motion_path,
459 get_interpolated_rotation_path,
460 get_antenna_pattern,
461 get_preview_links,
462 ])
463 .run(tauri::generate_context!())
464 .expect("error while running tauri application");
465}
466
467#[cfg(test)]
468mod tests {
469 use super::fers_api;
470
471 /// Verifies that the `libfers` C++ library is correctly linked.
472 ///
473 /// This test ensures that:
474 /// 1. The Rust linker can find the `libfers.a` static library.
475 /// 2. All required FFI symbols are present and callable.
476 /// 3. The `FersContext::new()` wrapper successfully creates a C++ context.
477 ///
478 /// If this test fails:
479 /// * Check that the `build.rs` script correctly specifies library search paths.
480 /// * Verify that the C++ libraries have been built by CMake.
481 /// * Ensure that `bindgen` generated the correct bindings from `api.h`.
482 ///
483 /// # Panics
484 ///
485 /// This test will panic if `FersContext::new()` returns `None`, indicating
486 /// a failure to allocate or initialize the C++ context.
487 #[test]
488 fn it_links_libfers_and_creates_context() {
489 // This test will fail at link time if the library is not found.
490 // It will fail at runtime if the symbols don't match or creation fails.
491 let _context = fers_api::FersContext::new().expect("FersContext::new() returned None");
492
493 // The context is automatically destroyed when it goes out of scope due to `Drop`.
494 // No explicit destroy call is needed.
495 }
496}