FERS 1.0.0
The Flexible Extensible Radar Simulator
Loading...
Searching...
No Matches
fers_api.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//! # Safe Rust FFI Wrapper for `libfers`
5//!
6//! This module provides a safe, idiomatic Rust interface to the `libfers` C++
7//! simulation library. It bridges the gap between the raw C-style FFI (generated
8//! by `bindgen`) and the higher-level Rust code used in the Tauri application.
9//!
10//! ## Safety Guarantees
11//!
12//! The module ensures the following safety properties:
13//!
14//! 1. **Automatic Resource Management**: All C-allocated resources (contexts, strings)
15//! are wrapped in RAII types that guarantee cleanup via `Drop`.
16//! 2. **Memory Safety**: Raw pointers from C are only dereferenced within `unsafe`
17//! blocks with documented safety invariants.
18//! 3. **String Conversion**: All C strings are converted to Rust `String`s with
19//! proper UTF-8 validation and null-termination handling.
20//! 4. **Thread Safety**: The `FersContext` is marked as `Send + Sync` because it
21//! will be protected by a `Mutex` in the application layer.
22//!
23//! ## Error Handling
24//!
25//! All fallible operations return `Result<T, String>`, where the error string
26//! contains a human-readable message retrieved from the C library's thread-local
27//! error storage.
28
29use std::ffi::{c_void, CStr, CString};
30use std::os::raw::c_char;
31use tauri::{AppHandle, Emitter};
32
33/// Raw FFI bindings generated by `bindgen` from `libfers/api.h`.
34///
35/// This inner module is kept private to prevent direct access to unsafe FFI
36/// functions. It contains the raw C function declarations and opaque struct types
37/// that mirror the C-API header.
38///
39/// # Safety
40///
41/// All items in this module are `unsafe` to use directly. They require:
42/// * Valid, non-null pointers for all context handles.
43/// * Proper null-termination for all C strings.
44/// * Manual memory management (allocation/deallocation).
45///
46/// The parent module (`fers_api`) provides safe wrappers that enforce these invariants.
47mod ffi {
48 #![allow(non_upper_case_globals)]
49 #![allow(non_camel_case_types)]
50 #![allow(non_snake_case)]
51 #![allow(dead_code)]
52 include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
53}
54
55/// A smart pointer wrapper for C-allocated strings returned from `libfers`.
56///
57/// This type ensures that the memory allocated by the C library (via `strdup`)
58/// is properly freed when the wrapper goes out of scope, preventing memory leaks.
59///
60/// # Memory Management
61///
62/// The wrapped pointer must have been allocated by a `libfers` API function that
63/// transfers ownership to the caller (e.g., `fers_get_scenario_as_json`). The
64/// `Drop` implementation calls `fers_free_string` to release the memory back to
65/// the C allocator.
66///
67/// # Null Handling
68///
69/// If the pointer is null, the wrapper treats it as an empty string. This simplifies
70/// error handling in cases where null indicates "no data" rather than an error.
71struct FersOwnedString(*mut c_char);
72
73impl Drop for FersOwnedString {
74 /// Frees the underlying C string by calling `fers_free_string`.
75 ///
76 /// This is automatically invoked when the wrapper goes out of scope,
77 /// ensuring that no manual cleanup is required by the caller.
78 ///
79 /// # Safety
80 ///
81 /// The pointer must have been allocated by `libfers` (typically via `strdup`)
82 /// and must not have been freed already. The `Drop` trait ensures this is
83 /// called exactly once per instance.
84 fn drop(&mut self) {
85 if !self.0.is_null() {
86 // SAFETY: The pointer was allocated by `libfers` and is valid until we call `fers_free_string`.
87 // We are the sole owner of this pointer.
88 unsafe { ffi::fers_free_string(self.0) };
89 }
90 }
91}
92
93impl FersOwnedString {
94 /// Converts the owned C string to a Rust `String`, consuming the wrapper.
95 ///
96 /// This method performs UTF-8 validation and copies the string data into a
97 /// Rust-managed `String`. The C-allocated memory is freed after the conversion.
98 ///
99 /// # Returns
100 ///
101 /// * `Ok(String)` - The converted string if valid UTF-8.
102 /// * `Err(std::str::Utf8Error)` - If the C string contains invalid UTF-8 bytes.
103 ///
104 /// # Example
105 ///
106 /// ```no_run
107 /// let owned = FersOwnedString(some_c_string_ptr);
108 /// match owned.into_string() {
109 /// Ok(s) => println!("Got string: {}", s),
110 /// Err(e) => eprintln!("Invalid UTF-8: {}", e),
111 /// }
112 /// ```
113 fn into_string(self) -> Result<String, std::str::Utf8Error> {
114 if self.0.is_null() {
115 return Ok(String::new());
116 }
117 // SAFETY: `self.0` is a valid, null-terminated C string from `libfers`.
118 // The `CStr::from_ptr` is safe as long as the pointer is valid.
119 let c_str = unsafe { CStr::from_ptr(self.0) };
120 c_str.to_str().map(|s| s.to_string())
121 }
122}
123
124/// A safe, RAII-style wrapper for the `fers_context_t*` C handle.
125///
126/// This struct encapsulates the lifetime and ownership of a simulation context
127/// created by the `libfers` C++ library. It ensures that:
128/// * The context is created via `fers_context_create` on initialization.
129/// * The context is destroyed via `fers_context_destroy` when dropped.
130/// * The context is never null after successful creation.
131///
132/// # Thread Safety
133///
134/// This type implements `Send` and `Sync` because the underlying C++ context will
135/// be protected by a `Mutex` in the Tauri application. The C++ `FersContext` class
136/// is not thread-safe, but by serializing all access through Rust's `Mutex`, we
137/// ensure that only one thread can call methods on the context at a time.
138///
139/// # Example
140///
141/// ```no_run
142/// use fers_api::FersContext;
143///
144/// let context = FersContext::new().expect("Failed to create context");
145/// context.load_scenario_from_xml_file("scenario.xml")?;
146/// let json = context.get_scenario_as_json()?;
147/// // Context is automatically destroyed when it goes out of scope
148/// ```
149pub struct FersContext {
150 /// The raw pointer to the C++ context object.
151 ///
152 /// This must be a raw pointer because `fers_context_t` is an opaque struct.
153 /// The `Send` and `Sync` traits are manually implemented because we'll wrap this
154 /// context in a Mutex, ensuring that access to the non-thread-safe C++ object
155 /// is properly synchronized.
156 ptr: *mut ffi::fers_context_t,
157}
158
159// SAFETY: The FersContext will be protected by a Mutex. All C-API calls on a single
160// context are not guaranteed to be thread-safe by themselves, but by enforcing
161// serialized access through a Mutex, we make its usage safe across threads.
162unsafe impl Send for FersContext {}
163unsafe impl Sync for FersContext {}
164
165impl Drop for FersContext {
166 /// Destroys the underlying C++ context and frees all associated resources.
167 ///
168 /// This method is automatically called when the `FersContext` goes out of scope.
169 /// It delegates to `fers_context_destroy`, which is responsible for cleaning up
170 /// the C++ `FersContext` object and its owned `World`.
171 ///
172 /// # Safety
173 ///
174 /// The pointer must be valid and non-null. The `Drop` trait guarantees this is
175 /// called exactly once, preventing double-free errors.
176 fn drop(&mut self) {
177 if !self.ptr.is_null() {
178 // SAFETY: `self.ptr` is a valid handle created by `fers_context_create`.
179 // The `Drop` trait ensures this is called exactly once.
180 unsafe { ffi::fers_context_destroy(self.ptr) };
181 }
182 }
183}
184
185/// Retrieves and formats the last error message from the `libfers` C-API.
186///
187/// This helper function is called whenever a C-API function returns an error code.
188/// It queries the thread-local error storage via `fers_get_last_error_message`,
189/// converts the C string to a Rust `String`, and ensures the memory is freed.
190///
191/// # Returns
192///
193/// A human-readable error message. If no error message is available (null pointer),
194/// a default message is returned. If the error message contains invalid UTF-8, an
195/// error describing the UTF-8 issue is returned instead.
196///
197/// # Thread Safety
198///
199/// This function is thread-safe because the error storage in `libfers` is thread-local.
200/// Each thread has its own error message buffer.
201fn get_last_error() -> String {
202 // SAFETY: `fers_get_last_error_message` is a thread-safe FFI function.
203 let error_ptr = unsafe { ffi::fers_get_last_error_message() };
204 if error_ptr.is_null() {
205 "An unknown FFI error occurred.".to_string()
206 } else {
207 // The FersOwnedString wrapper ensures the memory is freed.
208 FersOwnedString(error_ptr)
209 .into_string()
210 .unwrap_or_else(|e| format!("FFI error message contained invalid UTF-8: {}", e))
211 }
212}
213
214/// Data structure for simulation progress events emitted to the frontend.
215#[derive(serde::Serialize, Clone)]
216struct ProgressPayload {
217 message: String,
218 current: i32,
219 total: i32,
220}
221
222/// The C-style callback function passed to `fers_run_simulation`.
223///
224/// This function is invoked by the C++ core to report progress. It reconstructs the
225/// `AppHandle` from the `user_data` pointer and emits a Tauri event to the frontend.
226///
227/// # Safety
228///
229/// This function is marked `unsafe` because it dereferences raw pointers (`message`, `user_data`).
230/// The caller (the C++ library) must guarantee that `message` is a valid, null-terminated
231/// UTF-8 string and that `user_data` is a valid pointer to an `AppHandle`. The pointer
232/// is only valid for the duration of the `fers_run_simulation` call.
233#[allow(clippy::similar_names)]
234extern "C" fn simulation_progress_callback(
235 message: *const c_char,
236 current: i32,
237 total: i32,
238 user_data: *mut c_void,
239) {
240 if user_data.is_null() {
241 return;
242 }
243 // SAFETY: This is safe because we know `user_data` is a pointer to the AppHandle,
244 // which is guaranteed to be valid for the lifetime of the simulation call.
245 let app_handle = unsafe { &*(user_data as *const AppHandle) };
246
247 // SAFETY: `message` is guaranteed by the C-API to be a valid, null-terminated string.
248 let message_str = unsafe { CStr::from_ptr(message) }.to_string_lossy().into_owned();
249
250 let payload = ProgressPayload { message: message_str, current, total };
251
252 // Emit the event to the frontend. If this fails, there's little we can do
253 // from the callback, so we just let it panic in debug builds.
254 app_handle
255 .emit("simulation-progress", payload)
256 .expect("Failed to emit simulation-progress event");
257}
258
259/// A safe RAII wrapper for the antenna pattern data returned by the C-API.
260struct FersAntennaPatternData(*mut ffi::fers_antenna_pattern_data_t);
261impl Drop for FersAntennaPatternData {
262 fn drop(&mut self) {
263 if !self.0.is_null() {
264 // SAFETY: The pointer is valid and owned by this struct.
265 unsafe { ffi::fers_free_antenna_pattern_data(self.0) };
266 }
267 }
268}
269
270/// Data structure for antenna pattern data sent to the frontend.
271///
272/// This struct flattens the 2D gain data into a 1D vector for easy serialization.
273#[derive(serde::Serialize)]
274pub struct AntennaPatternData {
275 /// Flattened array of linear gain values (normalized 0.0 to 1.0).
276 /// Ordered row-major: Elevation rows, then Azimuth columns.
277 gains: Vec<f64>,
278 /// Number of samples along the azimuth axis (360 degrees).
279 az_count: usize,
280 /// Number of samples along the elevation axis (180 degrees).
281 el_count: usize,
282 /// The peak linear gain found in the pattern, used for normalization.
283 max_gain: f64,
284}
285
286// Helper wrapper for the visual link list
287struct FersVisualLinkList(*mut ffi::fers_visual_link_list_t);
288
289impl Drop for FersVisualLinkList {
290 fn drop(&mut self) {
291 if !self.0.is_null() {
292 unsafe { ffi::fers_free_preview_links(self.0) };
293 }
294 }
295}
296
297/// Represents a visual link segment for 3D rendering.
298///
299/// This struct maps C-style enums to integers for consumption by the TypeScript frontend.
300#[derive(serde::Serialize)]
301pub struct VisualLink {
302 /// The type of radio link.
303 /// * `0`: Monostatic (Tx -> Tgt -> Rx, where Tx==Rx)
304 /// * `1`: Bistatic Illuminator (Tx -> Tgt)
305 /// * `2`: Bistatic Scattered (Tgt -> Rx)
306 /// * `3`: Direct Interference (Tx -> Rx)
307 pub link_type: u8,
308 /// The radiometric quality of the link.
309 /// * `0`: Strong (SNR > 0 dB)
310 /// * `1`: Weak (SNR < 0 dB, visible but sub-noise)
311 pub quality: u8,
312 /// A pre-formatted label string (e.g., "-95 dBm").
313 pub label: String,
314 /// The name of the start component for this segment.
315 pub source_name: String,
316 /// The name of the end component for this segment.
317 pub dest_name: String,
318 /// The name of the original transmitter (useful for scattered paths).
319 pub origin_name: String,
320}
321
322impl FersContext {
323 /// Creates a new `FersContext` by calling the C-API constructor.
324 ///
325 /// This function allocates a new C++ `FersContext` object on the heap via
326 /// `fers_context_create`. The returned context is initially empty and must be
327 /// populated by loading a scenario (via `load_scenario_from_xml_file` or
328 /// `update_scenario_from_json`).
329 ///
330 /// # Returns
331 ///
332 /// * `Some(FersContext)` - If the context was successfully created.
333 /// * `None` - If allocation failed (e.g., out of memory) or if the C++ constructor
334 /// threw an exception.
335 ///
336 /// # Example
337 ///
338 /// ```no_run
339 /// let context = FersContext::new().expect("Failed to create FERS context");
340 /// ```
341 pub fn new() -> Option<Self> {
342 // SAFETY: `fers_context_create` is a simple constructor function with no preconditions.
343 let ptr = unsafe { ffi::fers_context_create() };
344 if ptr.is_null() {
345 None
346 } else {
347 Some(Self { ptr })
348 }
349 }
350
351 /// Loads a FERS scenario from an XML file into the context.
352 ///
353 /// This method replaces any existing scenario in the context with the one parsed
354 /// from the specified file. The XML is validated against the FERS schema if
355 /// validation is enabled in the C++ library.
356 ///
357 /// # Parameters
358 ///
359 /// * `filepath` - A UTF-8 string containing the absolute or relative path to the
360 /// FERS XML scenario file.
361 ///
362 /// # Returns
363 ///
364 /// * `Ok(())` - If the scenario was successfully loaded and parsed.
365 /// * `Err(String)` - If the file could not be read, the XML was invalid, or a
366 /// C++ exception was thrown. The error string contains details.
367 ///
368 /// # Example
369 ///
370 /// ```no_run
371 /// context.load_scenario_from_xml_file("/path/to/scenario.xml")?;
372 /// ```
373 pub fn load_scenario_from_xml_file(&self, filepath: &str) -> Result<(), String> {
374 let c_filepath = CString::new(filepath).map_err(|e| e.to_string())?;
375 // SAFETY: We pass a valid context pointer and a null-terminated C string.
376 // The function returns 0 on success.
377 let result =
378 unsafe { ffi::fers_load_scenario_from_xml_file(self.ptr, c_filepath.as_ptr(), 1) };
379 if result == 0 {
380 Ok(())
381 } else {
382 Err(get_last_error())
383 }
384 }
385
386 /// Retrieves the current in-memory scenario as a JSON string.
387 ///
388 /// This method serializes the C++ `World` object into JSON format, which mirrors
389 /// the structure used by the frontend. It is typically used to populate the UI
390 /// after loading a scenario from XML.
391 ///
392 /// # Returns
393 ///
394 /// * `Ok(String)` - The JSON representation of the scenario.
395 /// * `Err(String)` - If serialization failed or the JSON contains invalid UTF-8.
396 ///
397 /// # Memory Management
398 ///
399 /// The returned string is a Rust-owned `String`. The underlying C-allocated memory
400 /// is automatically freed by the `FersOwnedString` wrapper.
401 ///
402 /// # Example
403 ///
404 /// ```no_run
405 /// let json = context.get_scenario_as_json()?;
406 /// let scenario: serde_json::Value = serde_json::from_str(&json)?;
407 /// ```
408 pub fn get_scenario_as_json(&self) -> Result<String, String> {
409 // SAFETY: We pass a valid context pointer. The function returns a C string
410 // that we must free.
411 let json_ptr = unsafe { ffi::fers_get_scenario_as_json(self.ptr) };
412 if json_ptr.is_null() {
413 return Err(get_last_error());
414 }
415 // FersOwnedString takes ownership and will free the memory on drop.
416 FersOwnedString(json_ptr).into_string().map_err(|e| e.to_string())
417 }
418
419 /// Retrieves the current in-memory scenario as a FERS XML string.
420 ///
421 /// This method serializes the C++ `World` object into the standard FERS XML format.
422 /// It is typically used when the user wants to export the scenario (potentially
423 /// modified in the UI) back to a file.
424 ///
425 /// # Returns
426 ///
427 /// * `Ok(String)` - The XML representation of the scenario.
428 /// * `Err(String)` - If serialization failed or the XML contains invalid UTF-8.
429 ///
430 /// # Memory Management
431 ///
432 /// The returned string is a Rust-owned `String`. The underlying C-allocated memory
433 /// is automatically freed by the `FersOwnedString` wrapper.
434 ///
435 /// # Example
436 ///
437 /// ```no_run
438 /// let xml = context.get_scenario_as_xml()?;
439 /// std::fs::write("exported_scenario.xml", xml)?;
440 /// ```
441 pub fn get_scenario_as_xml(&self) -> Result<String, String> {
442 // SAFETY: We pass a valid context pointer. The function returns a C string
443 // that we must free.
444 let xml_ptr = unsafe { ffi::fers_get_scenario_as_xml(self.ptr) };
445 if xml_ptr.is_null() {
446 return Err(get_last_error());
447 }
448 // FersOwnedString takes ownership and will free the memory on drop.
449 FersOwnedString(xml_ptr).into_string().map_err(|e| e.to_string())
450 }
451
452 /// Updates the in-memory scenario from a JSON string.
453 ///
454 /// This method is the primary way for the UI to push modified scenario data back
455 /// to the C++ simulation engine. It deserializes the JSON and rebuilds the internal
456 /// `World` object, replacing any existing scenario.
457 ///
458 /// # Parameters
459 ///
460 /// * `json` - A UTF-8 JSON string representing the scenario. The structure must
461 /// match the schema expected by `libfers` (the same format returned by
462 /// `get_scenario_as_json`).
463 ///
464 /// # Returns
465 ///
466 /// * `Ok(())` - If the scenario was successfully deserialized and loaded.
467 /// * `Err(String)` - If the JSON was malformed, contained invalid data, or a C++
468 /// exception was thrown. The error string contains details.
469 ///
470 /// # Example
471 ///
472 /// ```no_run
473 /// let modified_json = /* JSON from UI */;
474 /// context.update_scenario_from_json(&modified_json)?;
475 /// ```
476 pub fn update_scenario_from_json(&self, json: &str) -> Result<(), String> {
477 let c_json = CString::new(json).map_err(|e| e.to_string())?;
478 // SAFETY: We pass a valid context pointer and a null-terminated C string.
479 // The function returns 0 on success.
480 let result = unsafe { ffi::fers_update_scenario_from_json(self.ptr, c_json.as_ptr()) };
481 if result == 0 {
482 Ok(())
483 } else {
484 Err(get_last_error())
485 }
486 }
487
488 /// Runs the simulation defined in the context.
489 ///
490 /// This is a blocking call that executes the simulation on a separate thread pool
491 /// managed by the C++ core. It accepts a Tauri `AppHandle` to enable progress
492 /// reporting via events.
493 ///
494 /// # Parameters
495 ///
496 /// * `app_handle` - A reference to the Tauri application handle, used for emitting events.
497 ///
498 /// # Returns
499 ///
500 /// * `Ok(())` - If the simulation completed successfully.
501 /// * `Err(String)` - If the simulation failed.
502 pub fn run_simulation(&self, app_handle: &AppHandle) -> Result<(), String> {
503 // The AppHandle is passed as a raw pointer through the `user_data` argument.
504 // This is safe because this function is blocking, and the app_handle reference
505 // will be valid for the entire duration of the C++ call.
506 let user_data_ptr = app_handle as *const _ as *mut c_void;
507
508 // SAFETY: We pass a valid context pointer, a valid function pointer for the callback,
509 // and a valid user_data pointer that points to the AppHandle.
510 let result = unsafe {
511 ffi::fers_run_simulation(self.ptr, Some(simulation_progress_callback), user_data_ptr)
512 };
513
514 if result == 0 {
515 Ok(())
516 } else {
517 Err(get_last_error())
518 }
519 }
520
521 /// Generates a KML file for the current scenario.
522 ///
523 /// # Parameters
524 ///
525 /// * `output_path` - The path where the KML file will be saved.
526 ///
527 /// # Returns
528 ///
529 /// * `Ok(())` - If the KML file was generated successfully.
530 /// * `Err(String)` - If KML generation failed.
531 pub fn generate_kml(&self, output_path: &str) -> Result<(), String> {
532 let c_output_path = CString::new(output_path).map_err(|e| e.to_string())?;
533 // SAFETY: We pass a valid context pointer and a null-terminated C string for the path.
534 let result = unsafe { ffi::fers_generate_kml(self.ptr, c_output_path.as_ptr()) };
535 if result == 0 {
536 Ok(())
537 } else {
538 Err(get_last_error())
539 }
540 }
541
542 /// Retrieves a sampled gain pattern for a specified antenna.
543 ///
544 /// # Parameters
545 ///
546 /// * `antenna_name` - The name of the antenna asset to sample.
547 /// * `az_samples` - The resolution along the azimuth axis.
548 /// * `el_samples` - The resolution along the elevation axis.
549 /// * `frequency` - The frequency in Hz to use for calculation.
550 ///
551 /// # Returns
552 ///
553 /// * `Ok(AntennaPatternData)` - If the pattern was successfully sampled.
554 /// * `Err(String)` - If the antenna was not found or an error occurred.
555 pub fn get_antenna_pattern(
556 &self,
557 antenna_name: &str,
558 az_samples: usize,
559 el_samples: usize,
560 frequency: f64,
561 ) -> Result<AntennaPatternData, String> {
562 let c_antenna_name = CString::new(antenna_name).map_err(|e| e.to_string())?;
563 // SAFETY: We pass a valid context pointer and valid arguments.
564 let result_ptr = unsafe {
565 ffi::fers_get_antenna_pattern(
566 self.ptr,
567 c_antenna_name.as_ptr(),
568 az_samples,
569 el_samples,
570 frequency,
571 )
572 };
573
574 if result_ptr.is_null() {
575 return Err(get_last_error());
576 }
577
578 let owned_data = FersAntennaPatternData(result_ptr);
579
580 // SAFETY: Dereferencing the non-null pointer returned by the FFI.
581 // The data is valid for the lifetime of `owned_data`.
582 let (gains_ptr, az_count, el_count, max_gain) = unsafe {
583 (
584 (*owned_data.0).gains,
585 (*owned_data.0).az_count,
586 (*owned_data.0).el_count,
587 (*owned_data.0).max_gain,
588 )
589 };
590 let total_samples = az_count * el_count;
591
592 // SAFETY: The gains_ptr is valid for `total_samples` elements.
593 let gains_slice = unsafe { std::slice::from_raw_parts(gains_ptr, total_samples) };
594
595 Ok(AntennaPatternData { gains: gains_slice.to_vec(), az_count, el_count, max_gain })
596 }
597
598 pub fn calculate_preview_links(&self, time: f64) -> Result<Vec<VisualLink>, String> {
599 let list_ptr = unsafe { ffi::fers_calculate_preview_links(self.ptr, time) };
600 if list_ptr.is_null() {
601 let err_msg = get_last_error();
602 // If we receive a NULL ptr but no error message, it might be a logic error
603 // or an unhandled edge case in C++. We default to the retrieved message.
604 return Err(err_msg);
605 }
606
607 let owned_list = FersVisualLinkList(list_ptr);
608 let count = unsafe { (*owned_list.0).count };
609 let links_ptr = unsafe { (*owned_list.0).links };
610
611 let mut result = Vec::with_capacity(count);
612 if count > 0 && !links_ptr.is_null() {
613 let slice = unsafe { std::slice::from_raw_parts(links_ptr, count) };
614 for l in slice {
615 let link_type_val = match l.type_ {
616 ffi::fers_link_type_t_FERS_LINK_MONOSTATIC => 0,
617 ffi::fers_link_type_t_FERS_LINK_BISTATIC_TX_TGT => 1,
618 ffi::fers_link_type_t_FERS_LINK_BISTATIC_TGT_RX => 2,
619 ffi::fers_link_type_t_FERS_LINK_DIRECT_TX_RX => 3,
620 _ => 0,
621 };
622
623 let quality_val = match l.quality {
624 ffi::fers_link_quality_t_FERS_LINK_STRONG => 0,
625 _ => 1,
626 };
627
628 let label =
629 unsafe { CStr::from_ptr(l.label.as_ptr()) }.to_string_lossy().into_owned();
630 let source_name = unsafe { CStr::from_ptr(l.source_name.as_ptr()) }
631 .to_string_lossy()
632 .into_owned();
633 let dest_name =
634 unsafe { CStr::from_ptr(l.dest_name.as_ptr()) }.to_string_lossy().into_owned();
635 let origin_name = unsafe { CStr::from_ptr(l.origin_name.as_ptr()) }
636 .to_string_lossy()
637 .into_owned();
638
639 result.push(VisualLink {
640 link_type: link_type_val,
641 quality: quality_val,
642 label,
643 source_name,
644 dest_name,
645 origin_name,
646 });
647 }
648 }
649 Ok(result)
650 }
651}
652
653/// A safe wrapper for the stateless `fers_get_interpolated_motion_path` C-API function.
654///
655/// This function converts Rust-native waypoint data into C-compatible types,
656/// calls the FFI function, and then converts the result back into a `Vec` of points,
657/// ensuring that all C-allocated memory is properly freed.
658///
659/// # Parameters
660/// * `waypoints` - A vector of motion waypoints from the frontend.
661/// * `interp_type` - The interpolation algorithm to use.
662/// * `num_points` - The desired number of points in the output path.
663///
664/// # Returns
665/// * `Ok(Vec<InterpolatedPoint>)` - A vector of points representing the calculated path.
666/// * `Err(String)` - An error message if the FFI call failed.
667pub fn get_interpolated_motion_path(
668 waypoints: Vec<crate::MotionWaypoint>,
669 interp_type: crate::InterpolationType,
670 num_points: usize,
671) -> Result<Vec<crate::InterpolatedMotionPoint>, String> {
672 if waypoints.is_empty() || num_points == 0 {
673 return Ok(Vec::new());
674 }
675
676 let c_waypoints: Vec<ffi::fers_motion_waypoint_t> = waypoints
677 .into_iter()
678 .map(|wp| ffi::fers_motion_waypoint_t { time: wp.time, x: wp.x, y: wp.y, z: wp.altitude })
679 .collect();
680
681 let c_interp_type = match interp_type {
682 crate::InterpolationType::Static => ffi::fers_interp_type_t_FERS_INTERP_STATIC,
683 crate::InterpolationType::Linear => ffi::fers_interp_type_t_FERS_INTERP_LINEAR,
684 crate::InterpolationType::Cubic => ffi::fers_interp_type_t_FERS_INTERP_CUBIC,
685 };
686
687 // SAFETY: We are calling the stateless FFI function with valid, well-formed arguments.
688 // The pointer returned is owned by us and must be freed.
689 let result_ptr = unsafe {
690 ffi::fers_get_interpolated_motion_path(
691 c_waypoints.as_ptr(),
692 c_waypoints.len(),
693 c_interp_type,
694 num_points,
695 )
696 };
697
698 if result_ptr.is_null() {
699 return Err(get_last_error());
700 }
701
702 // RAII wrapper to ensure the C-allocated path is freed.
703 struct FersInterpolatedMotionPath(*mut ffi::fers_interpolated_path_t);
704
705 impl Drop for FersInterpolatedMotionPath {
706 fn drop(&mut self) {
707 if !self.0.is_null() {
708 // SAFETY: The pointer is valid and owned by this struct.
709 unsafe { ffi::fers_free_interpolated_motion_path(self.0) };
710 }
711 }
712 }
713
714 let owned_path = FersInterpolatedMotionPath(result_ptr);
715
716 // SAFETY: We are accessing the fields of a non-null pointer returned by the FFI.
717 // The `count` and `points` fields are guaranteed to be valid for the lifetime of `owned_path`.
718 let result_slice =
719 unsafe { std::slice::from_raw_parts((*owned_path.0).points, (*owned_path.0).count) };
720
721 let points: Vec<crate::InterpolatedMotionPoint> = result_slice
722 .iter()
723 .map(|p| crate::InterpolatedMotionPoint {
724 x: p.x,
725 y: p.y,
726 z: p.z,
727 vx: p.vx,
728 vy: p.vy,
729 vz: p.vz,
730 })
731 .collect();
732
733 Ok(points)
734}
735
736/// A safe wrapper for the stateless `fers_get_interpolated_rotation_path` C-API function.
737///
738/// This function converts Rust-native rotation waypoints into C-compatible types,
739/// calls the FFI function, and converts the result back into a `Vec` of points.
740/// It handles the mapping between Rust enums and C enums for interpolation types
741/// and ensures that the C-allocated array is properly freed.
742///
743/// # Parameters
744/// * `waypoints` - A vector of `RotationWaypoint`s.
745/// * `interp_type` - The interpolation algorithm to use.
746/// * `num_points` - The desired number of points in the output path.
747///
748/// # Returns
749/// * `Ok(Vec<InterpolatedRotationPoint>)` - A vector of interpolated points.
750/// * `Err(String)` - An error message if the FFI call failed.
751pub fn get_interpolated_rotation_path(
752 waypoints: Vec<crate::RotationWaypoint>,
753 interp_type: crate::InterpolationType,
754 num_points: usize,
755) -> Result<Vec<crate::InterpolatedRotationPoint>, String> {
756 if waypoints.is_empty() || num_points == 0 {
757 return Ok(Vec::new());
758 }
759
760 let c_waypoints: Vec<ffi::fers_rotation_waypoint_t> = waypoints
761 .into_iter()
762 .map(|wp| ffi::fers_rotation_waypoint_t {
763 time: wp.time,
764 azimuth_deg: wp.azimuth,
765 elevation_deg: wp.elevation,
766 })
767 .collect();
768
769 let c_interp_type = match interp_type {
770 crate::InterpolationType::Static => ffi::fers_interp_type_t_FERS_INTERP_STATIC,
771 crate::InterpolationType::Linear => ffi::fers_interp_type_t_FERS_INTERP_LINEAR,
772 crate::InterpolationType::Cubic => ffi::fers_interp_type_t_FERS_INTERP_CUBIC,
773 };
774
775 let result_ptr = unsafe {
776 ffi::fers_get_interpolated_rotation_path(
777 c_waypoints.as_ptr(),
778 c_waypoints.len(),
779 c_interp_type,
780 num_points,
781 )
782 };
783
784 if result_ptr.is_null() {
785 return Err(get_last_error());
786 }
787
788 struct FersInterpolatedRotationPath(*mut ffi::fers_interpolated_rotation_path_t);
789
790 impl Drop for FersInterpolatedRotationPath {
791 fn drop(&mut self) {
792 if !self.0.is_null() {
793 unsafe { ffi::fers_free_interpolated_rotation_path(self.0) };
794 }
795 }
796 }
797
798 let owned_path = FersInterpolatedRotationPath(result_ptr);
799
800 let result_slice =
801 unsafe { std::slice::from_raw_parts((*owned_path.0).points, (*owned_path.0).count) };
802
803 let points: Vec<crate::InterpolatedRotationPoint> = result_slice
804 .iter()
805 .map(|p| crate::InterpolatedRotationPoint {
806 azimuth_deg: p.azimuth_deg,
807 elevation_deg: p.elevation_deg,
808 })
809 .collect();
810
811 Ok(points)
812}