FERS 0.1.0
The Flexible Extensible Radar Simulator
Loading...
Searching...
No Matches
api.cpp
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/**
5 * @file api.cpp
6 * @brief Implementation of the C-style FFI for the libfers core library.
7 *
8 * This file provides the C implementations for the functions declared in `api.h`.
9 * It acts as the bridge between the C ABI and the C++ core, handling object
10 * creation/destruction, exception catching, error reporting, and type casting.
11 */
12
13#include <algorithm>
14#include <cmath>
15#include <core/logging.h>
16#include <core/parameters.h>
17#include <core/sim_id.h>
18#include <cstddef>
19#include <cstdint>
20#include <filesystem>
21#include <format>
22#include <functional>
23#include <iterator>
24#include <libfers/api.h>
25#include <limits>
26#include <math/path.h>
27#include <math/rotation_path.h>
28#include <mutex>
29#include <nlohmann/json.hpp>
30#include <optional>
31#include <span>
32#include <string>
33#include <utility>
34#include <vector>
35
37#include "core/fers_context.h"
39#include "core/sim_threading.h"
40#include "core/thread_pool.h"
41#include "fers_version.h"
46#include "serial/xml_parser.h"
48#include "signal/radar_signal.h"
50
51// The fers_context struct is defined here as an alias for our C++ class.
52// This allows the C-API to return an opaque pointer, hiding the C++ implementation.
54{
55};
56
57// A thread-local error message string ensures that error details from one
58// thread's API call do not interfere with another's. This is crucial for a
59// thread-safe FFI layer.
60thread_local std::string last_error_message;
61thread_local std::vector<std::string> last_warning_messages;
62
63/**
64 * @brief Centralized exception handler for the C-API boundary.
65 *
66 * This function catches standard C++ exceptions, records their `what()` message
67 * into the thread-local error storage, and logs the error. This prevents C++
68 * exceptions from propagating across the FFI boundary, which would be undefined behavior.
69 * @param e The exception that was caught.
70 * @param function_name The name of the API function where the error occurred.
71 */
72static void handle_api_exception(const std::exception& e, const std::string& function_name)
73{
74 last_error_message = e.what();
75 LOG(logging::Level::ERROR, "API Error in {}: {}", function_name, last_error_message);
76}
77
83
88
94
95extern "C" {
96
98{
99 last_error_message.clear();
101 try
102 {
103 // NOLINTNEXTLINE(cppcoreguidelines-owning-memory): Public C API returns an owned handle.
104 return new fers_context_t();
105 }
106 catch (const std::bad_alloc& e)
107 {
108 handle_api_exception(e, "fers_context_create");
109 return nullptr;
110 }
111 catch (const std::exception& e)
112 {
113 handle_api_exception(e, "fers_context_create");
114 return nullptr;
115 }
116}
117
119{
120 if (context == nullptr)
121 {
122 return;
123 }
124 // NOLINTNEXTLINE(cppcoreguidelines-owning-memory): Frees handles allocated by `fers_context_create`.
125 delete context;
126}
127
128// Helper to map C enum to internal C++ enum
130{
131 switch (level)
132 {
133 case FERS_LOG_TRACE:
135 case FERS_LOG_DEBUG:
137 case FERS_LOG_INFO:
139 case FERS_LOG_WARNING:
141 case FERS_LOG_ERROR:
143 case FERS_LOG_FATAL:
145 case FERS_LOG_OFF:
146 return logging::Level::OFF;
147 default:
149 }
150}
151
153{
154 switch (level)
155 {
157 return FERS_LOG_TRACE;
159 return FERS_LOG_DEBUG;
161 return FERS_LOG_INFO;
163 return FERS_LOG_WARNING;
165 return FERS_LOG_ERROR;
167 return FERS_LOG_FATAL;
169 return FERS_LOG_OFF;
170 default:
171 return FERS_LOG_INFO;
172 }
173}
174
175namespace
176{
177 constexpr std::uint64_t nanoseconds_per_second = 1'000'000'000ULL;
178 constexpr std::uint64_t max_vrt_utc_epoch_ns =
179 static_cast<std::uint64_t>(std::numeric_limits<std::uint32_t>::max()) * nanoseconds_per_second +
180 (nanoseconds_per_second - 1ULL);
181
182 void set_api_error(std::string message)
183 {
184 last_error_message = std::move(message);
186 }
187
188 [[nodiscard]] bool is_valid_vita49_epoch(const std::uint64_t epoch_unix_nanoseconds) noexcept
189 {
190 return epoch_unix_nanoseconds <= max_vrt_utc_epoch_ns;
191 }
192
193 [[nodiscard]] bool is_valid_vita49_fullscale(const double fullscale) noexcept
194 {
195 return std::isfinite(fullscale) && fullscale > 0.0;
196 }
197
198 [[nodiscard]] bool is_valid_vita49_max_payload(const std::uint16_t max_udp_payload) noexcept
199 {
200 return max_udp_payload >= 64 && max_udp_payload <= 65507;
201 }
202
203 [[nodiscard]] std::optional<std::string> validate_vita49_config_for_run(const core::OutputConfig& config)
204 {
205 if (!core::isVita49Enabled(config))
206 {
207 return std::nullopt;
208 }
209 if (config.vita49.host.empty())
210 {
211 return "VITA49 endpoint host must be non-empty.";
212 }
213 if (config.vita49.port == 0)
214 {
215 return "VITA49 endpoint port must be in the range 1..65535.";
216 }
217 if (!is_valid_vita49_fullscale(config.vita49.adc_fullscale))
218 {
219 return "VITA49 fullscale must be a positive finite value.";
220 }
221 if (!is_valid_vita49_max_payload(config.vita49.max_udp_payload))
222 {
223 return "VITA49 max UDP payload must be between 64 and 65507 bytes.";
224 }
225 if (config.vita49.queue_depth == 0)
226 {
227 return "VITA49 queue depth must be greater than zero.";
228 }
229 if (config.vita49.epoch_unix_nanoseconds.has_value() &&
230 !is_valid_vita49_epoch(*config.vita49.epoch_unix_nanoseconds))
231 {
232 return "VITA49 epoch must fit the VRT 32-bit UTC seconds timestamp field.";
233 }
234 return std::nullopt;
235 }
236
237 void copy_visual_link_label(fers_visual_link_t& destination, const std::string& source) noexcept
238 {
239 const std::size_t count = std::min(source.size(), sizeof(destination.label) - 1);
240 std::copy_n(source.begin(), count, std::begin(destination.label));
241 destination.label[count] = '\0';
242 destination.label[sizeof(destination.label) - 1] = '\0';
243 }
244
245 [[nodiscard]] nlohmann::json stream_stats_to_json(const core::ReceiverStreamStats& stream)
246 {
247 auto timestamp_to_json = [](const std::optional<core::Vita49Timestamp>& timestamp) -> nlohmann::json
248 {
249 if (!timestamp.has_value())
250 {
251 return nullptr;
252 }
253 return {{"integer_seconds", timestamp->integer_seconds},
254 {"fractional_picoseconds", timestamp->fractional_picoseconds}};
255 };
256
257 return {
258 {"receiver_id", stream.receiver_id},
259 {"receiver_name", stream.receiver_name},
260 {"stream_id", stream.stream_id},
261 {"mode", stream.mode},
262 {"sample_rate", stream.sample_rate},
263 {"reference_frequency", stream.reference_frequency},
264 {"packets_emitted", stream.packets_emitted},
265 {"context_packets", stream.context_packets},
266 {"samples_emitted", stream.samples_emitted},
267 {"packets_dropped", stream.packets_dropped},
268 {"samples_dropped", stream.samples_dropped},
269 {"over_range_count", stream.over_range_count},
270 {"late_data_packet_count", stream.late_data_packet_count},
271 {"late_context_packet_count", stream.late_context_packet_count},
272 {"first_sample_time",
273 stream.first_sample_time.has_value() ? nlohmann::json(*stream.first_sample_time)
274 : nlohmann::json(nullptr)},
275 {"end_sample_time",
276 stream.end_sample_time.has_value() ? nlohmann::json(*stream.end_sample_time) : nlohmann::json(nullptr)},
277 {"first_timestamp", timestamp_to_json(stream.first_timestamp)},
278 {"end_timestamp", timestamp_to_json(stream.end_timestamp)}};
279 }
280
281 [[nodiscard]] std::string output_stats_to_json_string(const core::OutputStats& stats)
282 {
283 nlohmann::json streams = nlohmann::json::array();
284 for (const auto& stream : stats.streams)
285 {
286 streams.push_back(stream_stats_to_json(stream));
287 }
288
289 nlohmann::json result = {{"mode", stats.mode == core::OutputMode::Vita49Udp ? "vita49_udp" : "hdf5"},
290 {"epoch_unix_nanoseconds", nullptr},
291 {"streams", streams}};
292 if (stats.epoch_unix_nanoseconds.has_value())
293 {
294 result["epoch_unix_nanoseconds"] = std::to_string(*stats.epoch_unix_nanoseconds);
295 }
296 return result.dump();
297 }
298
299 [[nodiscard]] std::string
300 packet_trace_batch_to_json_string(std::span<const core::ReceiverOutputPacketTrace> packets)
301 {
302 auto timestamp_to_json = [](const std::optional<core::Vita49Timestamp>& timestamp) -> nlohmann::json
303 {
304 if (!timestamp.has_value())
305 {
306 return nullptr;
307 }
308 return {{"integer_seconds", timestamp->integer_seconds},
309 {"fractional_picoseconds", timestamp->fractional_picoseconds}};
310 };
311
312 nlohmann::json batch = nlohmann::json::array();
313 for (const auto& packet : packets)
314 {
315 batch.push_back({{"sequence", packet.sequence},
316 {"event", packet.event},
317 {"stream_id", packet.stream_id},
318 {"byte_count", packet.byte_count},
319 {"sample_count", packet.sample_count},
320 {"first_sample_time", packet.first_sample_time},
321 {"timestamp", timestamp_to_json(packet.timestamp)},
322 {"data_packet", packet.data_packet},
323 {"context_packet", packet.context_packet},
324 {"dropped", packet.dropped},
325 {"over_range", packet.over_range},
326 {"sample_loss", packet.sample_loss}});
327 }
328 return batch.dump();
329 }
330
331 std::mutex log_callback_mutex; ///< Guards C API log callback state.
332 fers_log_callback_t log_callback = nullptr; ///< Registered C API log callback, if any.
333 void* log_callback_user_data = nullptr; ///< Opaque user data passed to the registered log callback.
334
335 /// Forwards an internal formatted log line to the registered C API callback.
336 void forward_log_callback(const logging::Level level, const std::string& line, void* /*user_data*/)
337 {
338 fers_log_callback_t callback = nullptr;
339 void* user_data = nullptr;
340
341 {
342 std::scoped_lock const lock(log_callback_mutex);
343 callback = log_callback;
344 user_data = log_callback_user_data;
345 }
346
347 if (callback != nullptr)
348 {
349 callback(map_internal_log_level(level), line.c_str(), user_data);
350 }
351 }
352}
353
354int fers_configure_logging(fers_log_level_t level, const char* log_file_path)
355{
356 last_error_message.clear();
357 try
358 {
360 if ((log_file_path != nullptr) && ((*log_file_path) != 0))
361 {
362 auto result = logging::logger.logToFile(log_file_path);
363 if (!result)
364 {
365 last_error_message = result.error();
366 return 1;
367 }
368 }
369 return 0;
370 }
371 catch (const std::exception& e)
372 {
373 handle_api_exception(e, "fers_configure_logging");
374 return 1;
375 }
376}
377
378const char* fers_get_version(void) { return FERS_VERSION_STRING; }
379
381
382void fers_set_log_callback(fers_log_callback_t callback, void* user_data)
383{
384 {
385 std::scoped_lock const lock(log_callback_mutex);
386 log_callback = callback;
387 log_callback_user_data = user_data;
388 }
389
390 logging::logger.setCallback(callback == nullptr ? nullptr : forward_log_callback, nullptr);
391}
392
393void fers_log(fers_log_level_t level, const char* message)
394{
395 if (message == nullptr)
396 return;
397 // We pass a default source_location because C-API calls don't provide C++ source info
398 logging::logger.log(map_api_log_level(level), message, std::source_location::current());
399}
400
401int fers_set_thread_count(unsigned num_threads)
402{
403 last_error_message.clear();
404 try
405 {
406 if (auto res = params::setThreads(num_threads); !res)
407 {
408 last_error_message = res.error();
409 return 1;
410 }
411 return 0;
412 }
413 catch (const std::exception& e)
414 {
415 handle_api_exception(e, "fers_set_thread_count");
416 return 1;
417 }
418}
419
420int fers_set_output_directory(fers_context_t* context, const char* out_dir)
421{
422 last_error_message.clear();
423 if ((context == nullptr) || (out_dir == nullptr))
424 {
425 set_api_error("Invalid arguments: context or out_dir is NULL.");
426 return -1;
427 }
428 auto* ctx = context;
429 try
430 {
431 ctx->setOutputDir(out_dir);
432 return 0;
433 }
434 catch (const std::exception& e)
435 {
436 handle_api_exception(e, "fers_set_output_directory");
437 return 1;
438 }
439}
440
442{
443 last_error_message.clear();
444 if (context == nullptr)
445 {
446 set_api_error("Invalid arguments: context is NULL.");
447 return -1;
448 }
449
450 auto* ctx = context;
451 try
452 {
453 core::OutputConfig config = ctx->getOutputConfig();
455 ctx->setOutputConfig(std::move(config));
456 return 0;
457 }
458 catch (const std::exception& e)
459 {
460 handle_api_exception(e, "fers_use_hdf5_output");
461 return 1;
462 }
463}
464
465int fers_enable_vita49_udp_output(fers_context_t* context, const char* host, const std::uint16_t port)
466{
467 last_error_message.clear();
468 if (context == nullptr)
469 {
470 set_api_error("Invalid arguments: context is NULL.");
471 return -1;
472 }
473 if (host == nullptr)
474 {
475 set_api_error("Invalid VITA49 endpoint: host is NULL.");
476 return -1;
477 }
478 if (*host == '\0')
479 {
480 set_api_error("Invalid VITA49 endpoint: host must be non-empty.");
481 return 1;
482 }
483 if (port == 0)
484 {
485 set_api_error("Invalid VITA49 endpoint: port must be in the range 1..65535.");
486 return 1;
487 }
488
489 auto* ctx = context;
490 try
491 {
492 core::OutputConfig config = ctx->getOutputConfig();
494 config.vita49.host = host;
495 config.vita49.port = port;
496 ctx->setOutputConfig(std::move(config));
497 return 0;
498 }
499 catch (const std::exception& e)
500 {
501 handle_api_exception(e, "fers_enable_vita49_udp_output");
502 return 1;
503 }
504}
505
506int fers_set_vita49_fullscale(fers_context_t* context, const double fullscale)
507{
508 last_error_message.clear();
509 if (context == nullptr)
510 {
511 set_api_error("Invalid arguments: context is NULL.");
512 return -1;
513 }
514 if (!is_valid_vita49_fullscale(fullscale))
515 {
516 set_api_error("Invalid VITA49 fullscale: value must be positive and finite.");
517 return 1;
518 }
519
520 auto* ctx = context;
521 core::OutputConfig config = ctx->getOutputConfig();
522 config.vita49.adc_fullscale = static_cast<RealType>(fullscale);
523 ctx->setOutputConfig(std::move(config));
524 return 0;
525}
526
527int fers_set_vita49_epoch_unix_nanoseconds(fers_context_t* context, const std::uint64_t epoch_unix_nanoseconds)
528{
529 last_error_message.clear();
530 if (context == nullptr)
531 {
532 set_api_error("Invalid arguments: context is NULL.");
533 return -1;
534 }
535 if (!is_valid_vita49_epoch(epoch_unix_nanoseconds))
536 {
537 set_api_error("Invalid VITA49 epoch: value must fit the VRT 32-bit UTC seconds timestamp field.");
538 return 1;
539 }
540
541 auto* ctx = context;
542 core::OutputConfig config = ctx->getOutputConfig();
543 config.vita49.epoch_unix_nanoseconds = epoch_unix_nanoseconds;
544 ctx->setOutputConfig(std::move(config));
545 return 0;
546}
547
548int fers_set_vita49_max_udp_payload(fers_context_t* context, const std::uint16_t max_udp_payload)
549{
550 last_error_message.clear();
551 if (context == nullptr)
552 {
553 set_api_error("Invalid arguments: context is NULL.");
554 return -1;
555 }
556 if (!is_valid_vita49_max_payload(max_udp_payload))
557 {
558 set_api_error("Invalid VITA49 max UDP payload: value must be between 64 and 65507 bytes.");
559 return 1;
560 }
561
562 auto* ctx = context;
563 core::OutputConfig config = ctx->getOutputConfig();
564 config.vita49.max_udp_payload = max_udp_payload;
565 ctx->setOutputConfig(std::move(config));
566 return 0;
567}
568
569int fers_set_vita49_queue_depth(fers_context_t* context, const std::uint32_t queue_depth)
570{
571 last_error_message.clear();
572 if (context == nullptr)
573 {
574 set_api_error("Invalid arguments: context is NULL.");
575 return -1;
576 }
577 if (queue_depth == 0)
578 {
579 set_api_error("Invalid VITA49 queue depth: value must be greater than zero.");
580 return 1;
581 }
582
583 auto* ctx = context;
584 core::OutputConfig config = ctx->getOutputConfig();
585 config.vita49.queue_depth = queue_depth;
586 ctx->setOutputConfig(std::move(config));
587 return 0;
588}
589
591{
592 last_error_message.clear();
593 if (context == nullptr)
594 {
595 set_api_error("Invalid arguments: context is NULL.");
596 return -1;
597 }
598
599 auto* ctx = context;
600 core::OutputConfig config = ctx->getOutputConfig();
601 config.vita49.packet_trace_enabled = enabled != 0;
602 ctx->setOutputConfig(std::move(config));
603 return 0;
604}
605
606int fers_load_scenario_from_xml_file(fers_context_t* context, const char* xml_filepath, const int validate)
607{
608 last_error_message.clear();
610 if ((context == nullptr) || (xml_filepath == nullptr))
611 {
612 last_error_message = "Invalid arguments: context or xml_filepath is NULL.";
615 return -1;
616 }
617
618 auto* ctx = context;
619 try
620 {
621 // Set default output directory to the scenario file's directory
622 std::filesystem::path const p(xml_filepath);
623 auto parent = p.parent_path();
624 if (parent.empty())
625 parent = ".";
626 ctx->setOutputDir(parent.string());
627
628 serial::parseSimulation(xml_filepath, ctx->getWorld(), static_cast<bool>(validate), ctx->getMasterSeeder());
629
630 // After parsing, seed the master random number generator. This is done
631 // to ensure simulation reproducibility. If the scenario specifies a seed,
632 // it is used; otherwise, a non-deterministic seed is generated so that
633 // subsequent runs are unique by default.
634 if (params::params.random_seed)
635 {
636 LOG(logging::Level::INFO, "Using master seed from scenario file: {}", *params::params.random_seed);
637 ctx->getMasterSeeder().seed(*params::params.random_seed);
638 }
639 else
640 {
641 const auto seed = std::random_device{}();
642 LOG(logging::Level::INFO, "No master seed provided in scenario. Using random_device seed: {}", seed);
644 ctx->getMasterSeeder().seed(seed);
645 }
647 return 0; // Success
648 }
649 catch (const std::exception& e)
650 {
652 handle_api_exception(e, "fers_load_scenario_from_xml_file");
653 return 1; // Error
654 }
655}
656
657int fers_load_scenario_from_xml_string(fers_context_t* context, const char* xml_content, const int validate)
658{
659 last_error_message.clear();
661 if ((context == nullptr) || (xml_content == nullptr))
662 {
663 last_error_message = "Invalid arguments: context or xml_content is NULL.";
666 return -1;
667 }
668
669 auto* ctx = context;
670 try
671 {
672 serial::parseSimulationFromString(xml_content, ctx->getWorld(), static_cast<bool>(validate),
673 ctx->getMasterSeeder());
674
675 // After parsing, seed the master random number generator. This ensures
676 // that if the scenario provides a seed, the simulation will be
677 // reproducible. If not, a random seed is used to ensure unique runs.
678 if (params::params.random_seed)
679 {
680 LOG(logging::Level::INFO, "Using master seed from scenario string: {}", *params::params.random_seed);
681 ctx->getMasterSeeder().seed(*params::params.random_seed);
682 }
683 else
684 {
685 const auto seed = std::random_device{}();
686 LOG(logging::Level::INFO, "No master seed provided in scenario. Using random_device seed: {}", seed);
688 ctx->getMasterSeeder().seed(seed);
689 }
690
692 return 0; // Success
693 }
694 catch (const std::exception& e)
695 {
697 handle_api_exception(e, "fers_load_scenario_from_xml_string");
698 return 1; // Parsing or logic error
699 }
700}
701
703{
704 last_error_message.clear();
705 if (context == nullptr)
706 {
707 last_error_message = "Invalid context provided to fers_get_scenario_as_json.";
709 return nullptr;
710 }
711
712 const auto* ctx = context;
713 try
714 {
715 const nlohmann::json j = serial::world_to_json(*ctx->getWorld());
716 const std::string json_str = j.dump(2);
717 // A heap-allocated copy of the string is returned. This is necessary
718 // to transfer ownership of the memory across the FFI boundary to a
719 // client that will free it using `fers_free_string`.
720 return strdup(json_str.c_str());
721 }
722 catch (const std::exception& e)
723 {
724 handle_api_exception(e, "fers_get_scenario_as_json");
725 return nullptr;
726 }
727}
728
730{
731 last_error_message.clear();
732 if (context == nullptr)
733 {
734 last_error_message = "Invalid context provided to fers_get_scenario_as_xml.";
736 return nullptr;
737 }
738
739 const auto* ctx = context;
740 try
741 {
742 const std::string xml_str = serial::world_to_xml_string(*ctx->getWorld());
743 if (xml_str.empty())
744 {
745 throw std::runtime_error("XML serialization resulted in an empty string.");
746 }
747 // `strdup` is used to create a heap-allocated string that can be safely
748 // passed across the FFI boundary. The client is responsible for freeing
749 // this memory with `fers_free_string`.
750 return strdup(xml_str.c_str());
751 }
752 catch (const std::exception& e)
753 {
754 handle_api_exception(e, "fers_get_scenario_as_xml");
755 return nullptr;
756 }
757}
758
760{
761 last_error_message.clear();
762 if (context == nullptr)
763 {
764 last_error_message = "Invalid context provided to fers_get_last_output_metadata_json.";
766 return nullptr;
767 }
768
769 const auto* ctx = context;
770 try
771 {
772 const std::string json_str = ctx->getLastOutputMetadataJson();
773 return strdup(json_str.c_str());
774 }
775 catch (const std::exception& e)
776 {
777 handle_api_exception(e, "fers_get_last_output_metadata_json");
778 return nullptr;
779 }
780}
781
783{
784 last_error_message.clear();
785 if (context == nullptr)
786 {
787 last_error_message = "Invalid context provided to fers_get_memory_projection_json.";
789 return nullptr;
790 }
791
792 auto* ctx = context;
793
794 try
795 {
796 const auto projection = core::projectSimulationMemory(*ctx->getWorld());
797 const std::string json_str = core::memoryProjectionToJsonString(projection);
798 return strdup(json_str.c_str());
799 }
800 catch (const std::exception& e)
801 {
802 handle_api_exception(e, "fers_get_memory_projection_json");
803 return nullptr;
804 }
805}
806
807int fers_update_platform_from_json(fers_context_t* context, uint64_t id, const char* json)
808{
809 last_error_message.clear();
811 if ((context == nullptr) || (json == nullptr))
812 {
814 return -1;
815 }
816 auto* ctx = context;
817 try
818 {
819 auto* p = ctx->getWorld()->findPlatform(id);
820 if (p == nullptr)
821 {
822 last_error_message = "Platform not found";
824 return 1;
825 }
826 auto j = nlohmann::json::parse(json);
828 if (j.contains("name"))
829 {
830 p->setName(j.at("name").get<std::string>());
831 }
833 return 0;
834 }
835 catch (const std::exception& e)
836 {
838 handle_api_exception(e, "fers_update_platform_from_json");
839 return 1;
840 }
841}
842
844{
845 last_error_message.clear();
847 if ((context == nullptr) || (json == nullptr))
848 {
850 return -1;
851 }
852 auto* ctx = context;
853 try
854 {
855 auto j = nlohmann::json::parse(json);
856 serial::update_parameters_from_json(j, ctx->getMasterSeeder());
858 return 0;
859 }
860 catch (const std::exception& e)
861 {
863 handle_api_exception(e, "fers_update_parameters_from_json");
864 return 1;
865 }
866}
867
868int fers_update_antenna_from_json(fers_context_t* context, const char* json)
869{
870 last_error_message.clear();
871 if ((context == nullptr) || (json == nullptr))
872 return -1;
873 auto* ctx = context;
874 try
875 {
876 auto j = nlohmann::json::parse(json);
877 auto id = j.at("id").is_string() ? std::stoull(j.at("id").get<std::string>()) : j.at("id").get<uint64_t>();
878 auto* ant = ctx->getWorld()->findAntenna(id);
879 if (ant == nullptr)
880 {
881 last_error_message = "Antenna not found";
882 return 1;
883 }
884 serial::update_antenna_from_json(j, ant, *ctx->getWorld());
885 return 0;
886 }
887 catch (const std::exception& e)
888 {
889 handle_api_exception(e, "fers_update_antenna_from_json");
890 return 1;
891 }
892}
893
894int fers_update_waveform_from_json(fers_context_t* context, const char* json)
895{
896 last_error_message.clear();
897 if ((context == nullptr) || (json == nullptr))
898 return -1;
899 auto* ctx = context;
900 try
901 {
902 auto j = nlohmann::json::parse(json);
904 if (wf)
905 {
906 ctx->getWorld()->replace(std::move(wf));
907 }
908 return 0;
909 }
910 catch (const std::exception& e)
911 {
912 handle_api_exception(e, "fers_update_waveform_from_json");
913 return 1;
914 }
915}
916
917int fers_update_transmitter_from_json(fers_context_t* context, uint64_t id, const char* json)
918{
919 last_error_message.clear();
920 if ((context == nullptr) || (json == nullptr))
921 return -1;
922 auto* ctx = context;
923 try
924 {
925 auto* tx = ctx->getWorld()->findTransmitter(id);
926 if (tx == nullptr)
927 {
928 last_error_message = "Transmitter not found";
929 return 1;
930 }
931 auto j = nlohmann::json::parse(json);
932 serial::update_transmitter_from_json(j, tx, *ctx->getWorld(), ctx->getMasterSeeder());
933 return 0;
934 }
935 catch (const std::exception& e)
936 {
937 handle_api_exception(e, "fers_update_transmitter_from_json");
938 return 1;
939 }
940}
941
942int fers_update_receiver_from_json(fers_context_t* context, uint64_t id, const char* json)
943{
944 last_error_message.clear();
945 if ((context == nullptr) || (json == nullptr))
946 return -1;
947 auto* ctx = context;
948 try
949 {
950 auto* rx = ctx->getWorld()->findReceiver(id);
951 if (rx == nullptr)
952 {
953 last_error_message = "Receiver not found";
954 return 1;
955 }
956 auto j = nlohmann::json::parse(json);
957 serial::update_receiver_from_json(j, rx, *ctx->getWorld(), ctx->getMasterSeeder());
958 return 0;
959 }
960 catch (const std::exception& e)
961 {
962 handle_api_exception(e, "fers_update_receiver_from_json");
963 return 1;
964 }
965}
966
967int fers_update_target_from_json(fers_context_t* context, uint64_t id, const char* json)
968{
969 last_error_message.clear();
970 if ((context == nullptr) || (json == nullptr))
971 return -1;
972 auto* ctx = context;
973 try
974 {
975 auto* tgt = ctx->getWorld()->findTarget(id);
976 if (tgt == nullptr)
977 {
978 last_error_message = "Target not found";
979 return 1;
980 }
981 auto j = nlohmann::json::parse(json);
982 serial::update_target_from_json(j, tgt, *ctx->getWorld(), ctx->getMasterSeeder());
983 return 0;
984 }
985 catch (const std::exception& e)
986 {
987 handle_api_exception(e, "fers_update_target_from_json");
988 return 1;
989 }
990}
991
993{
994 last_error_message.clear();
995 if ((context == nullptr) || (json == nullptr))
996 return -1;
997 auto* ctx = context;
998 try
999 {
1000 auto j = nlohmann::json::parse(json);
1001 uint64_t const tx_id =
1002 j.at("tx_id").is_string() ? std::stoull(j.at("tx_id").get<std::string>()) : j.at("tx_id").get<uint64_t>();
1003 uint64_t const rx_id =
1004 j.at("rx_id").is_string() ? std::stoull(j.at("rx_id").get<std::string>()) : j.at("rx_id").get<uint64_t>();
1005 auto* tx = ctx->getWorld()->findTransmitter(tx_id);
1006 auto* rx = ctx->getWorld()->findReceiver(rx_id);
1007 if ((tx == nullptr) || (rx == nullptr))
1008 {
1009 last_error_message = "Monostatic components not found";
1010 return 1;
1011 }
1012 serial::update_monostatic_from_json(j, tx, rx, *ctx->getWorld(), ctx->getMasterSeeder());
1013 return 0;
1014 }
1015 catch (const std::exception& e)
1016 {
1017 handle_api_exception(e, "fers_update_monostatic_from_json");
1018 return 1;
1019 }
1020}
1021
1022int fers_update_timing_from_json(fers_context_t* context, uint64_t id, const char* json)
1023{
1024 last_error_message.clear();
1025 if ((context == nullptr) || (json == nullptr))
1026 return -1;
1027 auto* ctx = context;
1028 try
1029 {
1030 if (ctx->getWorld()->findTiming(id) == nullptr)
1031 {
1032 last_error_message = "Timing not found";
1033 return 1;
1034 }
1035 auto j = nlohmann::json::parse(json);
1036 serial::update_timing_from_json(j, *ctx->getWorld(), id);
1037 return 0;
1038 }
1039 catch (const std::exception& e)
1040 {
1041 handle_api_exception(e, "fers_update_timing_from_json");
1042 return 1;
1043 }
1044}
1045
1046int fers_update_scenario_from_json(fers_context_t* context, const char* scenario_json)
1047{
1048 last_error_message.clear();
1050 if ((context == nullptr) || (scenario_json == nullptr))
1051 {
1052 last_error_message = "Invalid arguments: context or scenario_json is NULL.";
1055 return -1;
1056 }
1057
1058 auto* ctx = context;
1059 try
1060 {
1061 const nlohmann::json j = nlohmann::json::parse(scenario_json);
1062 serial::json_to_world(j, *ctx->getWorld(), ctx->getMasterSeeder());
1064
1065 return 0; // Success
1066 }
1067 catch (const nlohmann::json::exception& e)
1068 {
1069 // A specific catch block for JSON errors is used to provide more
1070 // detailed feedback to the client (e.g., the UI), which can help
1071 // developers diagnose schema or data format issues more easily.
1072 last_error_message = "JSON parsing/deserialization error: " + std::string(e.what());
1073 LOG(logging::Level::ERROR, "API Error in {}: {}", "fers_update_scenario_from_json", last_error_message);
1075 return 2; // JSON error
1076 }
1077 catch (const std::exception& e)
1078 {
1080 handle_api_exception(e, "fers_update_scenario_from_json");
1081 return 1; // Generic error
1082 }
1083}
1084
1086{
1087 if (last_error_message.empty())
1088 {
1089 return nullptr; // No error to report
1090 }
1091 // `strdup` allocates with `malloc`, which is part of the C standard ABI,
1092 // making it safe to transfer ownership across the FFI boundary. The caller
1093 // must then free this memory using `fers_free_string`.
1094 // NOLINTNEXTLINE(cppcoreguidelines-no-malloc): C ABI string ownership is freed by `fers_free_string`.
1095 return strdup(last_error_message.c_str());
1096}
1097
1099{
1100 if (last_warning_messages.empty())
1101 {
1102 return nullptr;
1103 }
1104
1105 const std::string warning_json = nlohmann::json(last_warning_messages).dump();
1106 last_warning_messages.clear();
1107 return strdup(warning_json.c_str());
1108}
1109
1110void fers_free_string(char* str)
1111{
1112 if (str != nullptr)
1113 {
1114 // NOLINTNEXTLINE(cppcoreguidelines-owning-memory): Public C API frees strings returned by this library.
1115 free(str);
1116 }
1117}
1118
1119namespace
1120{
1121 struct SimulationRunRequest
1122 {
1123 fers_context_t* context;
1124 fers_progress_callback_t progress_callback;
1125 void* progress_user_data;
1126 fers_cancel_callback_t cancel_callback;
1127 void* cancel_user_data;
1128 fers_vita49_telemetry_callback_t vita49_telemetry_callback;
1129 void* vita49_telemetry_user_data;
1130 const char* function_name;
1131 const char* invalid_context_message;
1132 };
1133
1134 int run_simulation_common(const SimulationRunRequest& request)
1135 {
1136 last_error_message.clear();
1137 if (request.context == nullptr)
1138 {
1139 last_error_message = request.invalid_context_message;
1141 return -1;
1142 }
1143
1144 auto* ctx = request.context;
1145
1146 std::function<void(const std::string&, int, int)> progress_fn;
1147 if (request.progress_callback != nullptr)
1148 {
1149 progress_fn = [&request](const std::string& msg, const int current, const int total)
1150 { request.progress_callback(msg.c_str(), current, total, request.progress_user_data); };
1151 }
1152
1153 std::function<bool()> cancel_fn;
1154 if (request.cancel_callback != nullptr)
1155 {
1156 cancel_fn = [&request] { return request.cancel_callback(request.cancel_user_data) != 0; };
1157 }
1158
1160 if (request.vita49_telemetry_callback != nullptr)
1161 {
1162 telemetry_fn = [&request](const std::optional<core::OutputStats>& stats,
1163 std::span<const core::ReceiverOutputPacketTrace> packets)
1164 {
1165 std::string stats_json;
1166 std::string packet_batch_json;
1167 const char* stats_ptr = nullptr;
1168 const char* packet_batch_ptr = nullptr;
1169
1170 if (stats.has_value())
1171 {
1172 stats_json = output_stats_to_json_string(*stats);
1173 stats_ptr = stats_json.c_str();
1174 }
1175 if (!packets.empty())
1176 {
1177 packet_batch_json = packet_trace_batch_to_json_string(packets);
1178 packet_batch_ptr = packet_batch_json.c_str();
1179 }
1180
1181 request.vita49_telemetry_callback(stats_ptr, packet_batch_ptr, request.vita49_telemetry_user_data);
1182 };
1183 }
1184
1185 try
1186 {
1187 if (const auto validation_error = validate_vita49_config_for_run(ctx->getOutputConfig()))
1188 {
1189 set_api_error(*validation_error);
1190 return 1;
1191 }
1192
1194
1195 ctx->clearLastOutputMetadata();
1196 bool cancelled = false;
1197 auto output_metadata =
1198 core::runEventDrivenSim(ctx->getWorld(), pool, progress_fn, ctx->getOutputDir(), ctx->getOutputConfig(),
1199 std::move(cancel_fn), &cancelled, std::move(telemetry_fn));
1200 ctx->setLastOutputMetadata(output_metadata);
1201
1202 return cancelled ? 2 : 0;
1203 }
1204 catch (const std::exception& e)
1205 {
1206 handle_api_exception(e, request.function_name);
1207 return 1;
1208 }
1209 }
1210}
1211
1212int fers_run_simulation(fers_context_t* context, fers_progress_callback_t callback, void* user_data)
1213{
1214 return run_simulation_common(
1215 SimulationRunRequest{.context = context,
1216 .progress_callback = callback,
1217 .progress_user_data = user_data,
1218 .cancel_callback = nullptr,
1219 .cancel_user_data = nullptr,
1220 .vita49_telemetry_callback = nullptr,
1221 .vita49_telemetry_user_data = nullptr,
1222 .function_name = "fers_run_simulation",
1223 .invalid_context_message = "Invalid context provided to fers_run_simulation."});
1224}
1225
1227 void* progress_user_data, fers_cancel_callback_t cancel_callback, void* cancel_user_data,
1228 fers_vita49_telemetry_callback_t vita49_telemetry_callback, void* vita49_telemetry_user_data)
1229{
1230 return run_simulation_common(
1231 SimulationRunRequest{.context = context,
1232 .progress_callback = progress_callback,
1233 .progress_user_data = progress_user_data,
1234 .cancel_callback = cancel_callback,
1235 .cancel_user_data = cancel_user_data,
1236 .vita49_telemetry_callback = vita49_telemetry_callback,
1237 .vita49_telemetry_user_data = vita49_telemetry_user_data,
1238 .function_name = "fers_run_simulation_ex",
1239 .invalid_context_message = "Invalid context provided to fers_run_simulation_ex."});
1240}
1241
1242int fers_generate_kml(const fers_context_t* context, const char* output_kml_filepath)
1243{
1244 last_error_message.clear();
1245 if ((context == nullptr) || (output_kml_filepath == nullptr))
1246 {
1247 last_error_message = "Invalid arguments: context or output_kml_filepath is NULL.";
1249 return -1;
1250 }
1251
1252 const auto* ctx = context;
1253
1254 try
1255 {
1256 const auto result = serial::KmlGenerator::generateKml(*ctx->getWorld(), output_kml_filepath);
1257 if (result)
1258 {
1259 return 0; // Success
1260 }
1261
1262 last_error_message = result.error();
1264 return 2; // Generation failed
1265 }
1266 catch (const std::exception& e)
1267 {
1268 handle_api_exception(e, "fers_generate_kml");
1269 return 1; // Exception thrown
1270 }
1271}
1272
1273// --- Helper to convert C-API enum to C++ enum ---
1275{
1276 switch (type)
1277 {
1278 case FERS_INTERP_LINEAR:
1280 case FERS_INTERP_CUBIC:
1282 case FERS_INTERP_STATIC:
1283 default:
1285 }
1286}
1287
1301
1302
1304 const size_t waypoint_count,
1305 const fers_interp_type_t interp_type,
1306 const size_t num_points)
1307{
1308 last_error_message.clear();
1309 if ((waypoints == nullptr) || waypoint_count == 0 || num_points == 0)
1310 {
1311 last_error_message = "Invalid arguments: waypoints cannot be null and counts must be > 0.";
1313 return nullptr;
1314 }
1315 if (interp_type == FERS_INTERP_CUBIC && waypoint_count < 2)
1316 {
1317 last_error_message = "Cubic interpolation requires at least 2 waypoints.";
1319 return nullptr;
1320 }
1321
1322 try
1323 {
1324 math::Path path;
1325 path.setInterp(to_cpp_interp_type(interp_type));
1326
1327 for (size_t i = 0; i < waypoint_count; ++i)
1328 {
1329 math::Coord c;
1330 c.t = waypoints[i].time;
1331 c.pos.x = waypoints[i].x;
1332 c.pos.y = waypoints[i].y;
1333 c.pos.z = waypoints[i].z;
1334 path.addCoord(c);
1335 }
1336
1337 path.finalize();
1338
1339 // NOLINTNEXTLINE(cppcoreguidelines-owning-memory): Public C API returns an owned path struct.
1340 auto* result_path = new fers_interpolated_path_t();
1341 // NOLINTNEXTLINE(cppcoreguidelines-owning-memory): Public C API frees this array with the parent path.
1342 result_path->points = new fers_interpolated_point_t[num_points];
1343 result_path->count = num_points;
1344
1345 const double start_time = waypoints[0].time;
1346 const double end_time = waypoints[waypoint_count - 1].time;
1347 const double duration = end_time - start_time;
1348
1349 // Handle static case separately
1350 if (waypoint_count < 2 || duration <= 0)
1351 {
1352 const math::Vec3 pos = path.getPosition(start_time);
1353 for (size_t i = 0; i < num_points; ++i)
1354 {
1355 result_path->points[i] = {pos.x, pos.y, pos.z, 0.0, 0.0, 0.0};
1356 }
1357 return result_path;
1358 }
1359
1360 const double time_step =
1361 duration / static_cast<double>(num_points > 1 ? num_points - 1 : static_cast<size_t>(1));
1362
1363 for (size_t i = 0; i < num_points; ++i)
1364 {
1365 const double t = start_time + static_cast<double>(i) * time_step;
1366 const math::Vec3 pos = path.getPosition(t);
1367 const math::Vec3 vel = path.getVelocity(t);
1368 result_path->points[i] = {pos.x, pos.y, pos.z, vel.x, vel.y, vel.z};
1369 }
1370
1371 return result_path;
1372 }
1373 catch (const std::exception& e)
1374 {
1375 handle_api_exception(e, "fers_get_interpolated_motion_path");
1376 return nullptr;
1377 }
1378}
1379
1381{
1382 if (path != nullptr)
1383 {
1384 // NOLINTNEXTLINE(cppcoreguidelines-owning-memory): Frees arrays owned by C API path structs.
1385 delete[] path->points;
1386 // NOLINTNEXTLINE(cppcoreguidelines-owning-memory): Frees structs allocated by
1387 // `fers_get_interpolated_motion_path`.
1388 delete path;
1389 }
1390}
1391
1393 const size_t waypoint_count,
1394 const fers_interp_type_t interp_type,
1395 const fers_angle_unit_t angle_unit,
1396 const size_t num_points)
1397{
1398 last_error_message.clear();
1399 last_warning_messages.clear();
1401 if ((waypoints == nullptr) || waypoint_count == 0 || num_points == 0)
1402 {
1403 last_error_message = "Invalid arguments: waypoints cannot be null and counts must be > 0.";
1405 return nullptr;
1406 }
1407 if (interp_type == FERS_INTERP_CUBIC && waypoint_count < 2)
1408 {
1409 last_error_message = "Cubic interpolation requires at least 2 waypoints.";
1411 return nullptr;
1412 }
1413
1414 try
1415 {
1416 const auto unit =
1418 math::RotationPath path;
1419 path.setInterp(to_cpp_rot_interp_type(interp_type));
1420
1421 for (size_t i = 0; i < waypoint_count; ++i)
1422 {
1424 waypoints[i].azimuth, unit, serial::rotation_warning_utils::ValueKind::Angle, "C-API",
1425 std::format("rotation waypoint {}", i), "azimuth");
1427 waypoints[i].elevation, unit, serial::rotation_warning_utils::ValueKind::Angle, "C-API",
1428 std::format("rotation waypoint {}", i), "elevation");
1430 waypoints[i].azimuth, waypoints[i].elevation, waypoints[i].time, unit));
1431 }
1432
1433 path.finalize();
1434
1435 // NOLINTNEXTLINE(cppcoreguidelines-owning-memory): Public C API returns an owned path struct.
1436 auto* result_path = new fers_interpolated_rotation_path_t();
1437 // NOLINTNEXTLINE(cppcoreguidelines-owning-memory): Public C API frees this array with the parent path.
1438 result_path->points = new fers_interpolated_rotation_point_t[num_points];
1439 result_path->count = num_points;
1440
1441 const double start_time = waypoints[0].time;
1442 const double end_time = waypoints[waypoint_count - 1].time;
1443 const double duration = end_time - start_time;
1444
1445 // Handle static case separately
1446 if (waypoint_count < 2 || duration <= 0)
1447 {
1448 const math::SVec3 rot = path.getPosition(start_time);
1449 for (size_t i = 0; i < num_points; ++i)
1450 {
1451 result_path->points[i] = fers_interpolated_rotation_point_t{
1454 }
1455 return result_path;
1456 }
1457
1458 const double time_step =
1459 duration / static_cast<double>(num_points > 1 ? num_points - 1 : static_cast<size_t>(1));
1460
1461 for (size_t i = 0; i < num_points; ++i)
1462 {
1463 const double t = start_time + static_cast<double>(i) * time_step;
1464 const math::SVec3 rot = path.getPosition(t);
1465
1466 result_path->points[i] = fers_interpolated_rotation_point_t{
1469 }
1470
1471 return result_path;
1472 }
1473 catch (const std::exception& e)
1474 {
1476 handle_api_exception(e, "fers_get_interpolated_rotation_path");
1477 return nullptr;
1478 }
1479}
1480
1482{
1483 if (path != nullptr)
1484 {
1485 // NOLINTNEXTLINE(cppcoreguidelines-owning-memory): Frees arrays owned by C API path structs.
1486 delete[] path->points;
1487 // NOLINTNEXTLINE(cppcoreguidelines-owning-memory): Frees structs allocated by
1488 // `fers_get_interpolated_rotation_path`.
1489 delete path;
1490 }
1491}
1492
1493// --- Antenna Pattern Implementation ---
1494
1496 const size_t az_samples, const size_t el_samples,
1497 const double frequency_hz)
1498{
1499 last_error_message.clear();
1500 if ((context == nullptr) || az_samples < 2 || el_samples < 2)
1501 {
1502 last_error_message = "Invalid arguments: context must be non-null and sample counts must be >= 2.";
1504 return nullptr;
1505 }
1506
1507 try
1508 {
1509 const auto* ctx = context;
1510 antenna::Antenna const* ant = ctx->getWorld()->findAntenna(static_cast<SimId>(antenna_id));
1511
1512 if (ant == nullptr)
1513 {
1514 last_error_message = "Antenna ID '" + std::to_string(antenna_id) + "' not found in the world.";
1516 return nullptr;
1517 }
1518
1519 // TODO: Currently only using the first-found waveform. This is incorrect but also difficult to represent
1520 // correctly in scenarios with multiple waveforms as the gain for squarehorn and parabolic antennas
1521 // depends on the wavelength. Hence a decision needs to be made about whether to return multiple patterns
1522 // per waveform or have the user specify a representative wavelength in the UI per antenna.
1523 // Calculate wavelength from the provided frequency.
1524 // Default to 1GHz (0.3m) if frequency is invalid/zero, though the UI should prevent this
1525 // for antennas that strictly require it (Horn/Parabolic).
1526 RealType wavelength = 0.3;
1527 if (frequency_hz > 0.0)
1528 {
1529 wavelength = params::c() / frequency_hz;
1530 }
1531
1532 // NOLINTNEXTLINE(cppcoreguidelines-owning-memory): Public C API returns owned antenna pattern data.
1533 auto* data = new fers_antenna_pattern_data_t();
1534 data->az_count = az_samples;
1535 data->el_count = el_samples;
1536 const size_t total_samples = az_samples * el_samples;
1537 // NOLINTNEXTLINE(cppcoreguidelines-owning-memory): Public C API frees this array with the parent data.
1538 data->gains = new double[total_samples];
1539
1540 // The reference angle (boresight) is implicitly the local X-axis in the FERS engine.
1541 // We pass a zero rotation to get the gain relative to this boresight.
1542 const math::SVec3 ref_angle(1.0, 0.0, 0.0);
1543 double max_gain = 0.0;
1544
1545 const auto az_denominator = static_cast<RealType>(az_samples - 1);
1546 const auto el_denominator = static_cast<RealType>(el_samples - 1);
1547
1548 for (size_t i = 0; i < el_samples; ++i)
1549 {
1550 // Elevation from -PI/2 to PI/2
1551 const RealType elevation = (static_cast<RealType>(i) / el_denominator) * PI - (PI / 2.0);
1552 for (size_t j = 0; j < az_samples; ++j)
1553 {
1554 // Azimuth from -PI to PI
1555 const RealType azimuth = (static_cast<RealType>(j) / az_denominator) * 2.0 * PI - PI;
1556 const math::SVec3 sample_angle(1.0, azimuth, elevation);
1557 const RealType gain = ant->getGain(sample_angle, ref_angle, wavelength);
1558 data->gains[i * az_samples + j] = gain;
1559 max_gain = std::max(gain, max_gain);
1560 }
1561 }
1562
1563 data->max_gain = max_gain;
1564
1565 // Normalize the gains
1566 if (max_gain > 0)
1567 {
1568 for (size_t i = 0; i < total_samples; ++i)
1569 {
1570 data->gains[i] /= max_gain;
1571 }
1572 }
1573
1574 return data;
1575 }
1576 catch (const std::exception& e)
1577 {
1578 handle_api_exception(e, "fers_get_antenna_pattern");
1579 return nullptr;
1580 }
1581}
1582
1584{
1585 if (data != nullptr)
1586 {
1587 // NOLINTNEXTLINE(cppcoreguidelines-owning-memory): Frees arrays owned by C API pattern structs.
1588 delete[] data->gains;
1589 // NOLINTNEXTLINE(cppcoreguidelines-owning-memory): Frees structs allocated by `fers_get_antenna_pattern`.
1590 delete data;
1591 }
1592}
1593
1594// --- Preview Link Calculation Implementation ---
1595
1597{
1598 last_error_message.clear();
1599 if (context == nullptr)
1600 {
1601 last_error_message = "Invalid context passed to fers_calculate_preview_links";
1603 return nullptr;
1604 }
1605
1606 try
1607 {
1608 const auto* ctx = context;
1609 // Call the core physics logic in channel_model.cpp
1610 const auto cpp_links = simulation::calculatePreviewLinks(*ctx->getWorld(), time);
1611
1612 // Convert C++ vector to C-API struct
1613 // NOLINTNEXTLINE(cppcoreguidelines-owning-memory): Public C API returns owned preview-link lists.
1614 auto* result = new fers_visual_link_list_t();
1615 result->count = cpp_links.size();
1616
1617 if (!cpp_links.empty())
1618 {
1619 // NOLINTNEXTLINE(cppcoreguidelines-owning-memory): Public C API frees this array with the parent list.
1620 result->links = new fers_visual_link_t[result->count];
1621 for (size_t i = 0; i < result->count; ++i)
1622 {
1623 const auto& src = cpp_links[i];
1624 auto& dst = result->links[i];
1625
1626 // Map enums
1627 switch (src.type)
1628 {
1631 break;
1633 dst.type = FERS_LINK_BISTATIC_TX_TGT;
1634 break;
1636 dst.type = FERS_LINK_BISTATIC_TGT_RX;
1637 break;
1639 dst.type = FERS_LINK_DIRECT_TX_RX;
1640 break;
1641 }
1642
1643 dst.quality = (src.quality == simulation::LinkQuality::Strong) ? FERS_LINK_STRONG : FERS_LINK_WEAK;
1644
1645 copy_visual_link_label(dst, src.label);
1646
1647 dst.source_id = static_cast<uint64_t>(src.source_id);
1648 dst.dest_id = static_cast<uint64_t>(src.dest_id);
1649 dst.origin_id = static_cast<uint64_t>(src.origin_id);
1650 dst.rcs = src.rcs;
1651 dst.actual_power_dbm = src.actual_power_dbm;
1652 dst.display_value = src.display_value;
1653 }
1654 }
1655 else
1656 {
1657 result->links = nullptr;
1658 }
1659 return result;
1660 }
1661 catch (const std::exception& e)
1662 {
1663 handle_api_exception(e, "fers_calculate_preview_links");
1664 return nullptr;
1665 }
1666}
1667
1669{
1670 if (list != nullptr)
1671 {
1672 // NOLINTNEXTLINE(cppcoreguidelines-owning-memory): Frees arrays owned by C API preview-link lists.
1673 delete[] list->links;
1674 // NOLINTNEXTLINE(cppcoreguidelines-owning-memory): Frees structs allocated by `fers_calculate_preview_links`.
1675 delete list;
1676 }
1677}
1678}
Header file defining various types of antennas and their gain patterns.
static void discard_warning_capture() noexcept
Definition api.cpp:89
char * fers_get_scenario_as_xml(fers_context_t *context)
Serializes the current simulation scenario into a FERS XML string.
Definition api.cpp:729
int fers_update_antenna_from_json(fers_context_t *context, const char *json)
Updates a single antenna from JSON without full context recreation.
Definition api.cpp:868
int fers_run_simulation_ex(fers_context_t *context, fers_progress_callback_t progress_callback, void *progress_user_data, fers_cancel_callback_t cancel_callback, void *cancel_user_data, fers_vita49_telemetry_callback_t vita49_telemetry_callback, void *vita49_telemetry_user_data)
Runs the simulation with optional progress, cancellation, and VITA telemetry callbacks.
Definition api.cpp:1226
void fers_free_antenna_pattern_data(fers_antenna_pattern_data_t *data)
Frees the memory allocated for an antenna pattern data structure.
Definition api.cpp:1583
int fers_load_scenario_from_xml_string(fers_context_t *context, const char *xml_content, const int validate)
Loads a scenario into the context from a FERS XML string.
Definition api.cpp:657
void fers_log(fers_log_level_t level, const char *message)
Submits a log message to the library's unified logging system.
Definition api.cpp:393
char * fers_get_last_warning_messages_json()
Returns the last deduplicated rotation-unit warning list for the calling thread as JSON.
Definition api.cpp:1098
int fers_update_waveform_from_json(fers_context_t *context, const char *json)
Updates a single waveform from JSON without full context recreation.
Definition api.cpp:894
int fers_set_vita49_queue_depth(fers_context_t *context, const std::uint32_t queue_depth)
Definition api.cpp:569
int fers_update_transmitter_from_json(fers_context_t *context, uint64_t id, const char *json)
Updates a single transmitter from JSON without full context recreation.
Definition api.cpp:917
int fers_use_hdf5_output(fers_context_t *context)
Resets the context output mode to the default HDF5 output.
Definition api.cpp:441
char * fers_get_last_error_message()
Retrieves the last error message that occurred on the current thread.
Definition api.cpp:1085
int fers_enable_vita49_udp_output(fers_context_t *context, const char *host, const std::uint16_t port)
Definition api.cpp:465
void fers_free_interpolated_motion_path(fers_interpolated_path_t *path)
Frees the memory allocated for an interpolated motion path.
Definition api.cpp:1380
int fers_update_parameters_from_json(fers_context_t *context, const char *json)
Updates the global simulation parameters from JSON without full context recreation.
Definition api.cpp:843
int fers_generate_kml(const fers_context_t *context, const char *output_kml_filepath)
Generates a KML file for visualizing the scenario in the context.
Definition api.cpp:1242
char * fers_get_memory_projection_json(fers_context_t *context)
Returns a JSON projection of simulation startup memory and HDF5 payload size.
Definition api.cpp:782
int fers_set_output_directory(fers_context_t *context, const char *out_dir)
Sets the output directory for simulation results.
Definition api.cpp:420
void fers_context_destroy(fers_context_t *context)
Destroys a FERS simulation context and releases all associated memory.
Definition api.cpp:118
static void handle_api_exception(const std::exception &e, const std::string &function_name)
Centralized exception handler for the C-API boundary.
Definition api.cpp:72
fers_context_t * fers_context_create()
Creates a new FERS simulation context.
Definition api.cpp:97
int fers_update_target_from_json(fers_context_t *context, uint64_t id, const char *json)
Updates a single target from JSON without full context recreation.
Definition api.cpp:967
void fers_free_preview_links(fers_visual_link_list_t *list)
Frees the memory allocated for a preview link list.
Definition api.cpp:1668
int fers_set_vita49_max_udp_payload(fers_context_t *context, const std::uint16_t max_udp_payload)
Definition api.cpp:548
int fers_update_receiver_from_json(fers_context_t *context, uint64_t id, const char *json)
Updates a single receiver from JSON without full context recreation.
Definition api.cpp:942
fers_interpolated_path_t * fers_get_interpolated_motion_path(const fers_motion_waypoint_t *waypoints, const size_t waypoint_count, const fers_interp_type_t interp_type, const size_t num_points)
Calculates an interpolated motion path from a set of waypoints.
Definition api.cpp:1303
int fers_run_simulation(fers_context_t *context, fers_progress_callback_t callback, void *user_data)
Runs the simulation defined in the provided context.
Definition api.cpp:1212
void fers_free_string(char *str)
Frees a string that was allocated and returned by the libfers API.
Definition api.cpp:1110
fers_log_level_t fers_get_log_level()
Returns the current internal logger level.
Definition api.cpp:380
static void begin_warning_capture() noexcept
Definition api.cpp:78
math::RotationPath::InterpType to_cpp_rot_interp_type(const fers_interp_type_t type)
Definition api.cpp:1288
fers_antenna_pattern_data_t * fers_get_antenna_pattern(const fers_context_t *context, const uint64_t antenna_id, const size_t az_samples, const size_t el_samples, const double frequency_hz)
Samples the gain pattern of a specified antenna and provides the data.
Definition api.cpp:1495
const char * fers_get_version(void)
Returns the library version string.
Definition api.cpp:378
thread_local std::vector< std::string > last_warning_messages
Definition api.cpp:61
int fers_configure_logging(fers_log_level_t level, const char *log_file_path)
Configures the internal logger.
Definition api.cpp:354
void fers_free_interpolated_rotation_path(fers_interpolated_rotation_path_t *path)
Frees the memory allocated for an interpolated rotation path.
Definition api.cpp:1481
math::Path::InterpType to_cpp_interp_type(const fers_interp_type_t type)
Definition api.cpp:1274
void fers_set_log_callback(fers_log_callback_t callback, void *user_data)
Registers a callback for formatted log lines.
Definition api.cpp:382
int fers_load_scenario_from_xml_file(fers_context_t *context, const char *xml_filepath, const int validate)
Loads a scenario into the context from a FERS XML file.
Definition api.cpp:606
fers_visual_link_list_t * fers_calculate_preview_links(const fers_context_t *context, const double time)
Calculates visual links for a specific simulation time.
Definition api.cpp:1596
char * fers_get_scenario_as_json(fers_context_t *context)
Serializes the current simulation scenario into a JSON string.
Definition api.cpp:702
int fers_set_vita49_packet_trace_enabled(fers_context_t *context, const int enabled)
Enables or disables FERS VITA 49.2 packet trace telemetry.
Definition api.cpp:590
static void complete_warning_capture()
Definition api.cpp:84
int fers_update_monostatic_from_json(fers_context_t *context, const char *json)
Updates a monostatic radar from JSON without full context recreation.
Definition api.cpp:992
int fers_update_platform_from_json(fers_context_t *context, uint64_t id, const char *json)
Updates a single platform's paths and name from JSON without full context recreation.
Definition api.cpp:807
int fers_set_vita49_epoch_unix_nanoseconds(fers_context_t *context, const std::uint64_t epoch_unix_nanoseconds)
Definition api.cpp:527
int fers_update_timing_from_json(fers_context_t *context, uint64_t id, const char *json)
Updates a single timing source from JSON without full context recreation.
Definition api.cpp:1022
static logging::Level map_api_log_level(fers_log_level_t level)
Definition api.cpp:129
fers_interpolated_rotation_path_t * fers_get_interpolated_rotation_path(const fers_rotation_waypoint_t *waypoints, const size_t waypoint_count, const fers_interp_type_t interp_type, const fers_angle_unit_t angle_unit, const size_t num_points)
Calculates an interpolated rotation path from a set of waypoints.
Definition api.cpp:1392
static fers_log_level_t map_internal_log_level(logging::Level level)
Definition api.cpp:152
int fers_update_scenario_from_json(fers_context_t *context, const char *scenario_json)
Updates the simulation scenario from a JSON string.
Definition api.cpp:1046
char * fers_get_last_output_metadata_json(fers_context_t *context)
Returns JSON metadata for the most recent simulation output files.
Definition api.cpp:759
int fers_set_thread_count(unsigned num_threads)
Sets the number of worker threads for the simulation.
Definition api.cpp:401
int fers_set_vita49_fullscale(fers_context_t *context, const double fullscale)
Sets the fixed ADC full-scale value used by the FERS VITA 49.2 int16 IQ profile.
Definition api.cpp:506
thread_local std::string last_error_message
Definition api.cpp:60
fers_angle_unit_t
Units used for external rotation angles and rates.
Definition api.h:605
@ FERS_ANGLE_UNIT_RAD
Definition api.h:607
void(* fers_progress_callback_t)(const char *message, int current, int total, void *user_data)
A function pointer type for progress reporting callbacks.
Definition api.h:41
int(* fers_cancel_callback_t)(void *user_data)
A function pointer type for cooperative simulation cancellation.
Definition api.h:52
void(* fers_log_callback_t)(fers_log_level_t level, const char *line, void *user_data)
A function pointer type for receiving formatted log lines.
Definition api.h:210
fers_log_level_t
Log levels for the FERS library.
Definition api.h:193
@ FERS_LOG_FATAL
Fatal logging for unrecoverable failures.
Definition api.h:199
@ FERS_LOG_DEBUG
Debug-level diagnostic logging.
Definition api.h:195
@ FERS_LOG_ERROR
Error logging for failed operations.
Definition api.h:198
@ FERS_LOG_OFF
Disables logging output.
Definition api.h:200
@ FERS_LOG_INFO
Informational logging.
Definition api.h:196
@ FERS_LOG_TRACE
Trace-level diagnostic logging.
Definition api.h:194
@ FERS_LOG_WARNING
Warning logging for recoverable issues.
Definition api.h:197
fers_interp_type_t
Defines the interpolation methods available for path generation.
Definition api.h:595
@ FERS_INTERP_CUBIC
Definition api.h:598
@ FERS_INTERP_STATIC
Definition api.h:596
@ FERS_INTERP_LINEAR
Definition api.h:597
void(* fers_vita49_telemetry_callback_t)(const char *stats_json, const char *packet_batch_json, void *user_data)
A function pointer type for VITA 49.2 live telemetry callbacks.
Definition api.h:65
struct fers_context fers_context_t
Definition api.h:26
@ FERS_LINK_BISTATIC_TX_TGT
Definition api.h:743
@ FERS_LINK_MONOSTATIC
Definition api.h:742
@ FERS_LINK_BISTATIC_TGT_RX
Definition api.h:744
@ FERS_LINK_DIRECT_TX_RX
Definition api.h:745
@ FERS_LINK_WEAK
Definition api.h:734
@ FERS_LINK_STRONG
Definition api.h:733
Header for radar channel propagation and interaction models.
Manages the lifetime and state of a single FERS simulation scenario.
void setOutputDir(std::string dir)
Sets the output directory for simulation results.
core::World * getWorld() const noexcept
Retrieves a pointer to the simulation world.
std::string getLastOutputMetadataJson() const
Serializes the last simulation output metadata as JSON.
Abstract base class representing an antenna.
virtual RealType getGain(const math::SVec3 &angle, const math::SVec3 &refangle, RealType wavelength) const =0
Computes the gain of the antenna based on the input angle and reference angle.
radar::Target * findTarget(const SimId id)
Finds a target by ID.
Definition world.cpp:274
radar::Receiver * findReceiver(const SimId id)
Finds a receiver by ID.
Definition world.cpp:195
radar::Transmitter * findTransmitter(const SimId id)
Finds a transmitter by ID.
Definition world.cpp:181
radar::Platform * findPlatform(const SimId id)
Finds a platform by ID.
Definition world.cpp:171
void log(Level level, const std::string &message, const std::source_location &location=std::source_location::current()) noexcept
Logs a message with a specific log level and source location.
Definition logging.cpp:45
std::expected< void, std::string > logToFile(const std::string &filePath) noexcept
Sets the log file path to log messages to a file.
Definition logging.cpp:90
void setCallback(Callback callback, void *user_data) noexcept
Sets an optional callback that receives each formatted log line.
Definition logging.cpp:83
void setLevel(Level level) noexcept
Sets the logging level.
Definition logging.cpp:25
Represents a path with coordinates and allows for various interpolation methods.
Definition path.h:31
Vec3 getPosition(RealType t) const
Retrieves the position at a given time along the path.
Definition path.cpp:36
InterpType
Types of interpolation supported by the Path class.
Definition path.h:37
@ INTERP_STATIC
Hold the first coordinate for all query times.
@ INTERP_LINEAR
Linearly interpolate between neighboring coordinates.
@ INTERP_CUBIC
Cubically interpolate between neighboring coordinates.
void setInterp(InterpType settype) noexcept
Changes the interpolation type.
Definition path.cpp:164
Vec3 getVelocity(RealType t) const
Retrieves the velocity at a given time along the path.
Definition path.cpp:60
void addCoord(const Coord &coord) noexcept
Adds a coordinate to the path.
Definition path.cpp:27
void finalize()
Finalizes the path, preparing it for interpolation.
Definition path.cpp:147
Manages rotational paths with different interpolation techniques.
void finalize()
Finalizes the rotation path for interpolation.
void setInterp(InterpType setinterp) noexcept
Sets the interpolation type for the path.
SVec3 getPosition(RealType t) const
Gets the rotational position at a given time.
void addCoord(const RotationCoord &coord) noexcept
Adds a rotation coordinate to the path.
InterpType
Enumeration for types of interpolation.
@ INTERP_STATIC
Hold the first rotation for all query times.
@ INTERP_LINEAR
Linearly interpolate between neighboring rotations.
@ INTERP_CUBIC
Cubically interpolate between neighboring rotations.
A class representing a vector in spherical coordinates.
RealType elevation
The elevation angle of the vector.
RealType azimuth
The azimuth angle of the vector.
A class representing a vector in rectangular coordinates.
RealType x
The x component of the vector.
RealType z
The z component of the vector.
RealType y
The y component of the vector.
A simple thread pool implementation.
Definition thread_pool.h:29
static std::expected< void, std::string > generateKml(const core::World &world, const std::string &outputKmlPath)
Generates a KML file from a pre-built simulation world.
double RealType
Type for real numbers.
Definition config.h:27
constexpr RealType PI
Mathematical constant π (pi).
Definition config.h:43
Internal C++ class that encapsulates the state of a simulation instance.
Provides functions to serialize and deserialize the simulation world to/from JSON.
KML file generator for geographical visualization of FERS scenarios.
Header file for the logging system.
#define LOG(level,...)
Definition logging.h:19
Startup memory and output-size projection helpers for simulations.
OutputMetadata runEventDrivenSim(World *world, pool::ThreadPool &pool, const std::function< void(const std::string &, int, int)> &progress_callback, const std::string &output_dir, const OutputConfig &output_config, std::function< bool()> cancel_callback, bool *cancelled, ReceiverOutputTelemetryCallback telemetry_callback)
Runs the unified, event-driven radar simulation.
std::function< void(const std::optional< OutputStats > &, std::span< const ReceiverOutputPacketTrace >)> ReceiverOutputTelemetryCallback
std::string memoryProjectionToJsonString(const SimulationMemoryProjection &projection)
Serializes a simulation memory projection as JSON.
bool isVita49Enabled(const OutputConfig &config) noexcept
SimulationMemoryProjection projectSimulationMemory(const World &world)
Projects startup memory and rendered-output sizes for a simulation world.
@ WARNING
Warning level for potentially harmful situations.
@ FATAL
Fatal level for severe error events.
@ TRACE
Trace level for detailed debugging information.
@ INFO
Info level for informational messages.
@ OFF
Special level to disable all logging.
@ ERROR
Error level for error events.
@ DEBUG
Debug level for general debugging information.
Logger logger
Externally available logger object.
Definition logging.cpp:23
unsigned renderThreads() noexcept
Get the number of worker threads.
Definition parameters.h:145
std::expected< void, std::string > setThreads(const unsigned threads) noexcept
Set the number of worker threads.
Definition parameters.h:293
@ Radians
Compass azimuth and elevation expressed in radians.
@ Degrees
Compass azimuth and elevation expressed in degrees.
Parameters params
Global simulation parameter state.
Definition parameters.h:85
RealType c() noexcept
Get the speed of light.
Definition parameters.h:91
RealType internal_elevation_to_external(const RealType elevation, const params::RotationAngleUnit unit) noexcept
Converts an internal elevation angle to the external unit.
math::RotationCoord external_rotation_to_internal(const RealType azimuth, const RealType elevation, const RealType time, const params::RotationAngleUnit unit) noexcept
Converts external compass azimuth/elevation into internal rotation coordinates.
RealType internal_azimuth_to_external(const RealType azimuth, const params::RotationAngleUnit unit) noexcept
Converts an internal azimuth angle to the external compass convention.
std::vector< std::string > take_captured_warnings()
Returns and clears the thread-local captured rotation warnings.
void maybe_warn_about_rotation_value(const RealType value, const params::RotationAngleUnit declared_unit, const ValueKind kind, const std::string_view source, const std::string_view owner, const std::string_view field)
Emits or captures a warning when a rotation value likely uses the wrong unit.
void clear_captured_warnings() noexcept
Clears the thread-local captured rotation warnings.
void update_platform_paths_from_json(const nlohmann::json &j, radar::Platform *plat)
Updates a platform's motion and rotation paths from JSON.
void update_parameters_from_json(const nlohmann::json &j, std::mt19937 &masterSeeder)
Updates global simulation parameters from JSON.
void json_to_world(const nlohmann::json &j, core::World &world, std::mt19937 &masterSeeder)
Deserializes a nlohmann::json object and reconstructs the simulation world.
void update_receiver_from_json(const nlohmann::json &j, radar::Receiver *rx, core::World &world, std::mt19937 &)
Updates a receiver from JSON without full context recreation.
void update_timing_from_json(const nlohmann::json &j, core::World &world, const SimId id)
Updates a timing source from JSON without full context recreation.
void update_monostatic_from_json(const nlohmann::json &j, radar::Transmitter *tx, radar::Receiver *rx, core::World &world, std::mt19937 &masterSeeder)
Updates a monostatic radar from JSON without full context recreation.
void update_transmitter_from_json(const nlohmann::json &j, radar::Transmitter *tx, core::World &world, std::mt19937 &)
Updates a transmitter from JSON without full context recreation.
void parseSimulation(const std::string &filename, core::World *world, const bool validate, std::mt19937 &masterSeeder)
Parses a simulation configuration from an XML file.
void update_antenna_from_json(const nlohmann::json &j, antenna::Antenna *ant, core::World &world)
Updates an antenna from JSON without full context recreation.
void update_target_from_json(const nlohmann::json &j, radar::Target *existing_tgt, core::World &world, std::mt19937 &)
Updates a target from JSON without full context recreation.
nlohmann::json world_to_json(const core::World &world)
Serializes the entire simulation world into a nlohmann::json object.
void parseSimulationFromString(const std::string &xmlContent, core::World *world, const bool validate, std::mt19937 &masterSeeder)
Parses a simulation configuration directly from an XML string in memory.
std::string world_to_xml_string(const core::World &world)
Serializes the entire simulation world into an XML formatted string.
std::unique_ptr< fers_signal::RadarSignal > parse_waveform_from_json(const nlohmann::json &j)
Parses a Waveform from JSON.
@ DirectTxRx
Interference path.
@ Monostatic
Combined Tx/Rx path.
@ BistaticTgtRx
Scattered path.
@ BistaticTxTgt
Illuminator path.
std::vector< PreviewLink > calculatePreviewLinks(const core::World &world, const RealType time)
Calculates all visual links for the current world state at a specific time.
Defines the Parameters struct and provides methods for managing simulation parameters.
Provides the definition and functionality of the Path class for handling coordinate-based paths with ...
Classes for handling radar waveforms and signals.
Defines the RotationPath class for handling rotational paths with different interpolation types.
uint64_t SimId
64-bit Unique Simulation ID.
Definition sim_id.h:18
RealType c
Header file for the main simulation runner.
Vita49OutputConfig vita49
std::optional< std::uint64_t > epoch_unix_nanoseconds
std::optional< Vita49Timestamp > first_timestamp
std::optional< RealType > first_sample_time
std::optional< RealType > end_sample_time
std::uint64_t late_context_packet_count
std::uint64_t late_data_packet_count
std::optional< Vita49Timestamp > end_timestamp
std::uint16_t max_udp_payload
std::optional< std::uint64_t > epoch_unix_nanoseconds
Represents a sampled 2D antenna gain pattern.
Definition api.h:550
double * gains
Flat array of gain values [el_count * az_count].
Definition api.h:551
A container for an array of interpolated motion path points.
Definition api.h:663
fers_interpolated_point_t * points
Heap-allocated interpolated motion points.
Definition api.h:664
Represents a single interpolated point on a motion path.
Definition api.h:638
A container for an array of interpolated rotation path points.
Definition api.h:673
fers_interpolated_rotation_point_t * points
Heap-allocated interpolated rotation points.
Definition api.h:674
Represents a single interpolated point on a rotation path.
Definition api.h:652
Represents a single waypoint for a motion path.
Definition api.h:615
double x
X coordinate in meters (East in ENU).
Definition api.h:617
double time
Time in seconds.
Definition api.h:616
double y
Y coordinate in meters (North in ENU).
Definition api.h:618
double z
Z coordinate in meters (Up/Altitude in ENU).
Definition api.h:619
Represents a single waypoint for a rotation path.
Definition api.h:627
double time
Time in seconds.
Definition api.h:628
Represents a position in 3D space with an associated time.
Definition coord.h:24
RealType t
Time.
Definition coord.h:26
std::optional< unsigned > random_seed
Random seed for simulation.
Definition parameters.h:70
A simple thread pool implementation.
High-level facade for parsing XML configuration files into the FERS simulation environment.
Provides functions to serialize the simulation world back into the FERS XML format.