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_packet_count", stream.late_packet_count},
271 {"first_sample_time",
272 stream.first_sample_time.has_value() ? nlohmann::json(*stream.first_sample_time)
273 : nlohmann::json(nullptr)},
274 {"end_sample_time",
275 stream.end_sample_time.has_value() ? nlohmann::json(*stream.end_sample_time) : nlohmann::json(nullptr)},
276 {"first_timestamp", timestamp_to_json(stream.first_timestamp)},
277 {"end_timestamp", timestamp_to_json(stream.end_timestamp)}};
278 }
279
280 [[nodiscard]] std::string output_stats_to_json_string(const core::OutputStats& stats)
281 {
282 nlohmann::json streams = nlohmann::json::array();
283 for (const auto& stream : stats.streams)
284 {
285 streams.push_back(stream_stats_to_json(stream));
286 }
287
288 nlohmann::json result = {{"mode", stats.mode == core::OutputMode::Vita49Udp ? "vita49_udp" : "hdf5"},
289 {"epoch_unix_nanoseconds", nullptr},
290 {"streams", streams}};
291 if (stats.epoch_unix_nanoseconds.has_value())
292 {
293 result["epoch_unix_nanoseconds"] = std::to_string(*stats.epoch_unix_nanoseconds);
294 }
295 return result.dump();
296 }
297
298 [[nodiscard]] std::string
299 packet_trace_batch_to_json_string(std::span<const core::ReceiverOutputPacketTrace> packets)
300 {
301 auto timestamp_to_json = [](const std::optional<core::Vita49Timestamp>& timestamp) -> nlohmann::json
302 {
303 if (!timestamp.has_value())
304 {
305 return nullptr;
306 }
307 return {{"integer_seconds", timestamp->integer_seconds},
308 {"fractional_picoseconds", timestamp->fractional_picoseconds}};
309 };
310
311 nlohmann::json batch = nlohmann::json::array();
312 for (const auto& packet : packets)
313 {
314 batch.push_back({{"sequence", packet.sequence},
315 {"event", packet.event},
316 {"stream_id", packet.stream_id},
317 {"byte_count", packet.byte_count},
318 {"sample_count", packet.sample_count},
319 {"first_sample_time", packet.first_sample_time},
320 {"timestamp", timestamp_to_json(packet.timestamp)},
321 {"data_packet", packet.data_packet},
322 {"context_packet", packet.context_packet},
323 {"dropped", packet.dropped},
324 {"over_range", packet.over_range},
325 {"sample_loss", packet.sample_loss}});
326 }
327 return batch.dump();
328 }
329
330 std::mutex log_callback_mutex; ///< Guards C API log callback state.
331 fers_log_callback_t log_callback = nullptr; ///< Registered C API log callback, if any.
332 void* log_callback_user_data = nullptr; ///< Opaque user data passed to the registered log callback.
333
334 /// Forwards an internal formatted log line to the registered C API callback.
335 void forward_log_callback(const logging::Level level, const std::string& line, void* /*user_data*/)
336 {
337 fers_log_callback_t callback = nullptr;
338 void* user_data = nullptr;
339
340 {
341 std::scoped_lock const lock(log_callback_mutex);
342 callback = log_callback;
343 user_data = log_callback_user_data;
344 }
345
346 if (callback != nullptr)
347 {
348 callback(map_internal_log_level(level), line.c_str(), user_data);
349 }
350 }
351}
352
353int fers_configure_logging(fers_log_level_t level, const char* log_file_path)
354{
355 last_error_message.clear();
356 try
357 {
359 if ((log_file_path != nullptr) && ((*log_file_path) != 0))
360 {
361 auto result = logging::logger.logToFile(log_file_path);
362 if (!result)
363 {
364 last_error_message = result.error();
365 return 1;
366 }
367 }
368 return 0;
369 }
370 catch (const std::exception& e)
371 {
372 handle_api_exception(e, "fers_configure_logging");
373 return 1;
374 }
375}
376
377const char* fers_get_version(void) { return FERS_VERSION_STRING; }
378
380
381void fers_set_log_callback(fers_log_callback_t callback, void* user_data)
382{
383 {
384 std::scoped_lock const lock(log_callback_mutex);
385 log_callback = callback;
386 log_callback_user_data = user_data;
387 }
388
389 logging::logger.setCallback(callback == nullptr ? nullptr : forward_log_callback, nullptr);
390}
391
392void fers_log(fers_log_level_t level, const char* message)
393{
394 if (message == nullptr)
395 return;
396 // We pass a default source_location because C-API calls don't provide C++ source info
397 logging::logger.log(map_api_log_level(level), message, std::source_location::current());
398}
399
400int fers_set_thread_count(unsigned num_threads)
401{
402 last_error_message.clear();
403 try
404 {
405 if (auto res = params::setThreads(num_threads); !res)
406 {
407 last_error_message = res.error();
408 return 1;
409 }
410 return 0;
411 }
412 catch (const std::exception& e)
413 {
414 handle_api_exception(e, "fers_set_thread_count");
415 return 1;
416 }
417}
418
419int fers_set_output_directory(fers_context_t* context, const char* out_dir)
420{
421 last_error_message.clear();
422 if ((context == nullptr) || (out_dir == nullptr))
423 {
424 set_api_error("Invalid arguments: context or out_dir is NULL.");
425 return -1;
426 }
427 auto* ctx = context;
428 try
429 {
430 ctx->setOutputDir(out_dir);
431 return 0;
432 }
433 catch (const std::exception& e)
434 {
435 handle_api_exception(e, "fers_set_output_directory");
436 return 1;
437 }
438}
439
441{
442 last_error_message.clear();
443 if (context == nullptr)
444 {
445 set_api_error("Invalid arguments: context is NULL.");
446 return -1;
447 }
448
449 auto* ctx = context;
450 try
451 {
452 core::OutputConfig config = ctx->getOutputConfig();
454 ctx->setOutputConfig(std::move(config));
455 return 0;
456 }
457 catch (const std::exception& e)
458 {
459 handle_api_exception(e, "fers_use_hdf5_output");
460 return 1;
461 }
462}
463
464int fers_enable_vita49_udp_output(fers_context_t* context, const char* host, const std::uint16_t port)
465{
466 last_error_message.clear();
467 if (context == nullptr)
468 {
469 set_api_error("Invalid arguments: context is NULL.");
470 return -1;
471 }
472 if (host == nullptr)
473 {
474 set_api_error("Invalid VITA49 endpoint: host is NULL.");
475 return -1;
476 }
477 if (*host == '\0')
478 {
479 set_api_error("Invalid VITA49 endpoint: host must be non-empty.");
480 return 1;
481 }
482 if (port == 0)
483 {
484 set_api_error("Invalid VITA49 endpoint: port must be in the range 1..65535.");
485 return 1;
486 }
487
488 auto* ctx = context;
489 try
490 {
491 core::OutputConfig config = ctx->getOutputConfig();
493 config.vita49.host = host;
494 config.vita49.port = port;
495 ctx->setOutputConfig(std::move(config));
496 return 0;
497 }
498 catch (const std::exception& e)
499 {
500 handle_api_exception(e, "fers_enable_vita49_udp_output");
501 return 1;
502 }
503}
504
505int fers_set_vita49_fullscale(fers_context_t* context, const double fullscale)
506{
507 last_error_message.clear();
508 if (context == nullptr)
509 {
510 set_api_error("Invalid arguments: context is NULL.");
511 return -1;
512 }
513 if (!is_valid_vita49_fullscale(fullscale))
514 {
515 set_api_error("Invalid VITA49 fullscale: value must be positive and finite.");
516 return 1;
517 }
518
519 auto* ctx = context;
520 core::OutputConfig config = ctx->getOutputConfig();
521 config.vita49.adc_fullscale = static_cast<RealType>(fullscale);
522 ctx->setOutputConfig(std::move(config));
523 return 0;
524}
525
526int fers_set_vita49_epoch_unix_nanoseconds(fers_context_t* context, const std::uint64_t epoch_unix_nanoseconds)
527{
528 last_error_message.clear();
529 if (context == nullptr)
530 {
531 set_api_error("Invalid arguments: context is NULL.");
532 return -1;
533 }
534 if (!is_valid_vita49_epoch(epoch_unix_nanoseconds))
535 {
536 set_api_error("Invalid VITA49 epoch: value must fit the VRT 32-bit UTC seconds timestamp field.");
537 return 1;
538 }
539
540 auto* ctx = context;
541 core::OutputConfig config = ctx->getOutputConfig();
542 config.vita49.epoch_unix_nanoseconds = epoch_unix_nanoseconds;
543 ctx->setOutputConfig(std::move(config));
544 return 0;
545}
546
547int fers_set_vita49_max_udp_payload(fers_context_t* context, const std::uint16_t max_udp_payload)
548{
549 last_error_message.clear();
550 if (context == nullptr)
551 {
552 set_api_error("Invalid arguments: context is NULL.");
553 return -1;
554 }
555 if (!is_valid_vita49_max_payload(max_udp_payload))
556 {
557 set_api_error("Invalid VITA49 max UDP payload: value must be between 64 and 65507 bytes.");
558 return 1;
559 }
560
561 auto* ctx = context;
562 core::OutputConfig config = ctx->getOutputConfig();
563 config.vita49.max_udp_payload = max_udp_payload;
564 ctx->setOutputConfig(std::move(config));
565 return 0;
566}
567
568int fers_set_vita49_queue_depth(fers_context_t* context, const std::uint32_t queue_depth)
569{
570 last_error_message.clear();
571 if (context == nullptr)
572 {
573 set_api_error("Invalid arguments: context is NULL.");
574 return -1;
575 }
576 if (queue_depth == 0)
577 {
578 set_api_error("Invalid VITA49 queue depth: value must be greater than zero.");
579 return 1;
580 }
581
582 auto* ctx = context;
583 core::OutputConfig config = ctx->getOutputConfig();
584 config.vita49.queue_depth = queue_depth;
585 ctx->setOutputConfig(std::move(config));
586 return 0;
587}
588
590{
591 last_error_message.clear();
592 if (context == nullptr)
593 {
594 set_api_error("Invalid arguments: context is NULL.");
595 return -1;
596 }
597
598 auto* ctx = context;
599 core::OutputConfig config = ctx->getOutputConfig();
600 config.vita49.packet_trace_enabled = enabled != 0;
601 ctx->setOutputConfig(std::move(config));
602 return 0;
603}
604
605int fers_load_scenario_from_xml_file(fers_context_t* context, const char* xml_filepath, const int validate)
606{
607 last_error_message.clear();
609 if ((context == nullptr) || (xml_filepath == nullptr))
610 {
611 last_error_message = "Invalid arguments: context or xml_filepath is NULL.";
614 return -1;
615 }
616
617 auto* ctx = context;
618 try
619 {
620 // Set default output directory to the scenario file's directory
621 std::filesystem::path const p(xml_filepath);
622 auto parent = p.parent_path();
623 if (parent.empty())
624 parent = ".";
625 ctx->setOutputDir(parent.string());
626
627 serial::parseSimulation(xml_filepath, ctx->getWorld(), static_cast<bool>(validate), ctx->getMasterSeeder());
628
629 // After parsing, seed the master random number generator. This is done
630 // to ensure simulation reproducibility. If the scenario specifies a seed,
631 // it is used; otherwise, a non-deterministic seed is generated so that
632 // subsequent runs are unique by default.
633 if (params::params.random_seed)
634 {
635 LOG(logging::Level::INFO, "Using master seed from scenario file: {}", *params::params.random_seed);
636 ctx->getMasterSeeder().seed(*params::params.random_seed);
637 }
638 else
639 {
640 const auto seed = std::random_device{}();
641 LOG(logging::Level::INFO, "No master seed provided in scenario. Using random_device seed: {}", seed);
643 ctx->getMasterSeeder().seed(seed);
644 }
646 return 0; // Success
647 }
648 catch (const std::exception& e)
649 {
651 handle_api_exception(e, "fers_load_scenario_from_xml_file");
652 return 1; // Error
653 }
654}
655
656int fers_load_scenario_from_xml_string(fers_context_t* context, const char* xml_content, const int validate)
657{
658 last_error_message.clear();
660 if ((context == nullptr) || (xml_content == nullptr))
661 {
662 last_error_message = "Invalid arguments: context or xml_content is NULL.";
665 return -1;
666 }
667
668 auto* ctx = context;
669 try
670 {
671 serial::parseSimulationFromString(xml_content, ctx->getWorld(), static_cast<bool>(validate),
672 ctx->getMasterSeeder());
673
674 // After parsing, seed the master random number generator. This ensures
675 // that if the scenario provides a seed, the simulation will be
676 // reproducible. If not, a random seed is used to ensure unique runs.
677 if (params::params.random_seed)
678 {
679 LOG(logging::Level::INFO, "Using master seed from scenario string: {}", *params::params.random_seed);
680 ctx->getMasterSeeder().seed(*params::params.random_seed);
681 }
682 else
683 {
684 const auto seed = std::random_device{}();
685 LOG(logging::Level::INFO, "No master seed provided in scenario. Using random_device seed: {}", seed);
687 ctx->getMasterSeeder().seed(seed);
688 }
689
691 return 0; // Success
692 }
693 catch (const std::exception& e)
694 {
696 handle_api_exception(e, "fers_load_scenario_from_xml_string");
697 return 1; // Parsing or logic error
698 }
699}
700
702{
703 last_error_message.clear();
704 if (context == nullptr)
705 {
706 last_error_message = "Invalid context provided to fers_get_scenario_as_json.";
708 return nullptr;
709 }
710
711 const auto* ctx = context;
712 try
713 {
714 const nlohmann::json j = serial::world_to_json(*ctx->getWorld());
715 const std::string json_str = j.dump(2);
716 // A heap-allocated copy of the string is returned. This is necessary
717 // to transfer ownership of the memory across the FFI boundary to a
718 // client that will free it using `fers_free_string`.
719 return strdup(json_str.c_str());
720 }
721 catch (const std::exception& e)
722 {
723 handle_api_exception(e, "fers_get_scenario_as_json");
724 return nullptr;
725 }
726}
727
729{
730 last_error_message.clear();
731 if (context == nullptr)
732 {
733 last_error_message = "Invalid context provided to fers_get_scenario_as_xml.";
735 return nullptr;
736 }
737
738 const auto* ctx = context;
739 try
740 {
741 const std::string xml_str = serial::world_to_xml_string(*ctx->getWorld());
742 if (xml_str.empty())
743 {
744 throw std::runtime_error("XML serialization resulted in an empty string.");
745 }
746 // `strdup` is used to create a heap-allocated string that can be safely
747 // passed across the FFI boundary. The client is responsible for freeing
748 // this memory with `fers_free_string`.
749 return strdup(xml_str.c_str());
750 }
751 catch (const std::exception& e)
752 {
753 handle_api_exception(e, "fers_get_scenario_as_xml");
754 return nullptr;
755 }
756}
757
759{
760 last_error_message.clear();
761 if (context == nullptr)
762 {
763 last_error_message = "Invalid context provided to fers_get_last_output_metadata_json.";
765 return nullptr;
766 }
767
768 const auto* ctx = context;
769 try
770 {
771 const std::string json_str = ctx->getLastOutputMetadataJson();
772 return strdup(json_str.c_str());
773 }
774 catch (const std::exception& e)
775 {
776 handle_api_exception(e, "fers_get_last_output_metadata_json");
777 return nullptr;
778 }
779}
780
782{
783 last_error_message.clear();
784 if (context == nullptr)
785 {
786 last_error_message = "Invalid context provided to fers_get_memory_projection_json.";
788 return nullptr;
789 }
790
791 auto* ctx = context;
792
793 try
794 {
795 const auto projection = core::projectSimulationMemory(*ctx->getWorld());
796 const std::string json_str = core::memoryProjectionToJsonString(projection);
797 return strdup(json_str.c_str());
798 }
799 catch (const std::exception& e)
800 {
801 handle_api_exception(e, "fers_get_memory_projection_json");
802 return nullptr;
803 }
804}
805
806int fers_update_platform_from_json(fers_context_t* context, uint64_t id, const char* json)
807{
808 last_error_message.clear();
810 if ((context == nullptr) || (json == nullptr))
811 {
813 return -1;
814 }
815 auto* ctx = context;
816 try
817 {
818 auto* p = ctx->getWorld()->findPlatform(id);
819 if (p == nullptr)
820 {
821 last_error_message = "Platform not found";
823 return 1;
824 }
825 auto j = nlohmann::json::parse(json);
827 if (j.contains("name"))
828 {
829 p->setName(j.at("name").get<std::string>());
830 }
832 return 0;
833 }
834 catch (const std::exception& e)
835 {
837 handle_api_exception(e, "fers_update_platform_from_json");
838 return 1;
839 }
840}
841
843{
844 last_error_message.clear();
846 if ((context == nullptr) || (json == nullptr))
847 {
849 return -1;
850 }
851 auto* ctx = context;
852 try
853 {
854 auto j = nlohmann::json::parse(json);
855 serial::update_parameters_from_json(j, ctx->getMasterSeeder());
857 return 0;
858 }
859 catch (const std::exception& e)
860 {
862 handle_api_exception(e, "fers_update_parameters_from_json");
863 return 1;
864 }
865}
866
867int fers_update_antenna_from_json(fers_context_t* context, const char* json)
868{
869 last_error_message.clear();
870 if ((context == nullptr) || (json == nullptr))
871 return -1;
872 auto* ctx = context;
873 try
874 {
875 auto j = nlohmann::json::parse(json);
876 auto id = j.at("id").is_string() ? std::stoull(j.at("id").get<std::string>()) : j.at("id").get<uint64_t>();
877 auto* ant = ctx->getWorld()->findAntenna(id);
878 if (ant == nullptr)
879 {
880 last_error_message = "Antenna not found";
881 return 1;
882 }
883 serial::update_antenna_from_json(j, ant, *ctx->getWorld());
884 return 0;
885 }
886 catch (const std::exception& e)
887 {
888 handle_api_exception(e, "fers_update_antenna_from_json");
889 return 1;
890 }
891}
892
893int fers_update_waveform_from_json(fers_context_t* context, const char* json)
894{
895 last_error_message.clear();
896 if ((context == nullptr) || (json == nullptr))
897 return -1;
898 auto* ctx = context;
899 try
900 {
901 auto j = nlohmann::json::parse(json);
903 if (wf)
904 {
905 ctx->getWorld()->replace(std::move(wf));
906 }
907 return 0;
908 }
909 catch (const std::exception& e)
910 {
911 handle_api_exception(e, "fers_update_waveform_from_json");
912 return 1;
913 }
914}
915
916int fers_update_transmitter_from_json(fers_context_t* context, uint64_t id, const char* json)
917{
918 last_error_message.clear();
919 if ((context == nullptr) || (json == nullptr))
920 return -1;
921 auto* ctx = context;
922 try
923 {
924 auto* tx = ctx->getWorld()->findTransmitter(id);
925 if (tx == nullptr)
926 {
927 last_error_message = "Transmitter not found";
928 return 1;
929 }
930 auto j = nlohmann::json::parse(json);
931 serial::update_transmitter_from_json(j, tx, *ctx->getWorld(), ctx->getMasterSeeder());
932 return 0;
933 }
934 catch (const std::exception& e)
935 {
936 handle_api_exception(e, "fers_update_transmitter_from_json");
937 return 1;
938 }
939}
940
941int fers_update_receiver_from_json(fers_context_t* context, uint64_t id, const char* json)
942{
943 last_error_message.clear();
944 if ((context == nullptr) || (json == nullptr))
945 return -1;
946 auto* ctx = context;
947 try
948 {
949 auto* rx = ctx->getWorld()->findReceiver(id);
950 if (rx == nullptr)
951 {
952 last_error_message = "Receiver not found";
953 return 1;
954 }
955 auto j = nlohmann::json::parse(json);
956 serial::update_receiver_from_json(j, rx, *ctx->getWorld(), ctx->getMasterSeeder());
957 return 0;
958 }
959 catch (const std::exception& e)
960 {
961 handle_api_exception(e, "fers_update_receiver_from_json");
962 return 1;
963 }
964}
965
966int fers_update_target_from_json(fers_context_t* context, uint64_t id, const char* json)
967{
968 last_error_message.clear();
969 if ((context == nullptr) || (json == nullptr))
970 return -1;
971 auto* ctx = context;
972 try
973 {
974 auto* tgt = ctx->getWorld()->findTarget(id);
975 if (tgt == nullptr)
976 {
977 last_error_message = "Target not found";
978 return 1;
979 }
980 auto j = nlohmann::json::parse(json);
981 serial::update_target_from_json(j, tgt, *ctx->getWorld(), ctx->getMasterSeeder());
982 return 0;
983 }
984 catch (const std::exception& e)
985 {
986 handle_api_exception(e, "fers_update_target_from_json");
987 return 1;
988 }
989}
990
992{
993 last_error_message.clear();
994 if ((context == nullptr) || (json == nullptr))
995 return -1;
996 auto* ctx = context;
997 try
998 {
999 auto j = nlohmann::json::parse(json);
1000 uint64_t const tx_id =
1001 j.at("tx_id").is_string() ? std::stoull(j.at("tx_id").get<std::string>()) : j.at("tx_id").get<uint64_t>();
1002 uint64_t const rx_id =
1003 j.at("rx_id").is_string() ? std::stoull(j.at("rx_id").get<std::string>()) : j.at("rx_id").get<uint64_t>();
1004 auto* tx = ctx->getWorld()->findTransmitter(tx_id);
1005 auto* rx = ctx->getWorld()->findReceiver(rx_id);
1006 if ((tx == nullptr) || (rx == nullptr))
1007 {
1008 last_error_message = "Monostatic components not found";
1009 return 1;
1010 }
1011 serial::update_monostatic_from_json(j, tx, rx, *ctx->getWorld(), ctx->getMasterSeeder());
1012 return 0;
1013 }
1014 catch (const std::exception& e)
1015 {
1016 handle_api_exception(e, "fers_update_monostatic_from_json");
1017 return 1;
1018 }
1019}
1020
1021int fers_update_timing_from_json(fers_context_t* context, uint64_t id, const char* json)
1022{
1023 last_error_message.clear();
1024 if ((context == nullptr) || (json == nullptr))
1025 return -1;
1026 auto* ctx = context;
1027 try
1028 {
1029 if (ctx->getWorld()->findTiming(id) == nullptr)
1030 {
1031 last_error_message = "Timing not found";
1032 return 1;
1033 }
1034 auto j = nlohmann::json::parse(json);
1035 serial::update_timing_from_json(j, *ctx->getWorld(), id);
1036 return 0;
1037 }
1038 catch (const std::exception& e)
1039 {
1040 handle_api_exception(e, "fers_update_timing_from_json");
1041 return 1;
1042 }
1043}
1044
1045int fers_update_scenario_from_json(fers_context_t* context, const char* scenario_json)
1046{
1047 last_error_message.clear();
1049 if ((context == nullptr) || (scenario_json == nullptr))
1050 {
1051 last_error_message = "Invalid arguments: context or scenario_json is NULL.";
1054 return -1;
1055 }
1056
1057 auto* ctx = context;
1058 try
1059 {
1060 const nlohmann::json j = nlohmann::json::parse(scenario_json);
1061 serial::json_to_world(j, *ctx->getWorld(), ctx->getMasterSeeder());
1063
1064 return 0; // Success
1065 }
1066 catch (const nlohmann::json::exception& e)
1067 {
1068 // A specific catch block for JSON errors is used to provide more
1069 // detailed feedback to the client (e.g., the UI), which can help
1070 // developers diagnose schema or data format issues more easily.
1071 last_error_message = "JSON parsing/deserialization error: " + std::string(e.what());
1072 LOG(logging::Level::ERROR, "API Error in {}: {}", "fers_update_scenario_from_json", last_error_message);
1074 return 2; // JSON error
1075 }
1076 catch (const std::exception& e)
1077 {
1079 handle_api_exception(e, "fers_update_scenario_from_json");
1080 return 1; // Generic error
1081 }
1082}
1083
1085{
1086 if (last_error_message.empty())
1087 {
1088 return nullptr; // No error to report
1089 }
1090 // `strdup` allocates with `malloc`, which is part of the C standard ABI,
1091 // making it safe to transfer ownership across the FFI boundary. The caller
1092 // must then free this memory using `fers_free_string`.
1093 // NOLINTNEXTLINE(cppcoreguidelines-no-malloc): C ABI string ownership is freed by `fers_free_string`.
1094 return strdup(last_error_message.c_str());
1095}
1096
1098{
1099 if (last_warning_messages.empty())
1100 {
1101 return nullptr;
1102 }
1103
1104 const std::string warning_json = nlohmann::json(last_warning_messages).dump();
1105 last_warning_messages.clear();
1106 return strdup(warning_json.c_str());
1107}
1108
1109void fers_free_string(char* str)
1110{
1111 if (str != nullptr)
1112 {
1113 // NOLINTNEXTLINE(cppcoreguidelines-owning-memory): Public C API frees strings returned by this library.
1114 free(str);
1115 }
1116}
1117
1118namespace
1119{
1120 struct SimulationRunRequest
1121 {
1122 fers_context_t* context;
1123 fers_progress_callback_t progress_callback;
1124 void* progress_user_data;
1125 fers_cancel_callback_t cancel_callback;
1126 void* cancel_user_data;
1127 fers_vita49_telemetry_callback_t vita49_telemetry_callback;
1128 void* vita49_telemetry_user_data;
1129 const char* function_name;
1130 const char* invalid_context_message;
1131 };
1132
1133 int run_simulation_common(const SimulationRunRequest& request)
1134 {
1135 last_error_message.clear();
1136 if (request.context == nullptr)
1137 {
1138 last_error_message = request.invalid_context_message;
1140 return -1;
1141 }
1142
1143 auto* ctx = request.context;
1144
1145 std::function<void(const std::string&, int, int)> progress_fn;
1146 if (request.progress_callback != nullptr)
1147 {
1148 progress_fn = [&request](const std::string& msg, const int current, const int total)
1149 { request.progress_callback(msg.c_str(), current, total, request.progress_user_data); };
1150 }
1151
1152 std::function<bool()> cancel_fn;
1153 if (request.cancel_callback != nullptr)
1154 {
1155 cancel_fn = [&request] { return request.cancel_callback(request.cancel_user_data) != 0; };
1156 }
1157
1159 if (request.vita49_telemetry_callback != nullptr)
1160 {
1161 telemetry_fn = [&request](const std::optional<core::OutputStats>& stats,
1162 std::span<const core::ReceiverOutputPacketTrace> packets)
1163 {
1164 std::string stats_json;
1165 std::string packet_batch_json;
1166 const char* stats_ptr = nullptr;
1167 const char* packet_batch_ptr = nullptr;
1168
1169 if (stats.has_value())
1170 {
1171 stats_json = output_stats_to_json_string(*stats);
1172 stats_ptr = stats_json.c_str();
1173 }
1174 if (!packets.empty())
1175 {
1176 packet_batch_json = packet_trace_batch_to_json_string(packets);
1177 packet_batch_ptr = packet_batch_json.c_str();
1178 }
1179
1180 request.vita49_telemetry_callback(stats_ptr, packet_batch_ptr, request.vita49_telemetry_user_data);
1181 };
1182 }
1183
1184 try
1185 {
1186 if (const auto validation_error = validate_vita49_config_for_run(ctx->getOutputConfig()))
1187 {
1188 set_api_error(*validation_error);
1189 return 1;
1190 }
1191
1193
1194 ctx->clearLastOutputMetadata();
1195 bool cancelled = false;
1196 auto output_metadata =
1197 core::runEventDrivenSim(ctx->getWorld(), pool, progress_fn, ctx->getOutputDir(), ctx->getOutputConfig(),
1198 std::move(cancel_fn), &cancelled, std::move(telemetry_fn));
1199 ctx->setLastOutputMetadata(output_metadata);
1200
1201 return cancelled ? 2 : 0;
1202 }
1203 catch (const std::exception& e)
1204 {
1205 handle_api_exception(e, request.function_name);
1206 return 1;
1207 }
1208 }
1209}
1210
1211int fers_run_simulation(fers_context_t* context, fers_progress_callback_t callback, void* user_data)
1212{
1213 return run_simulation_common(
1214 SimulationRunRequest{.context = context,
1215 .progress_callback = callback,
1216 .progress_user_data = user_data,
1217 .cancel_callback = nullptr,
1218 .cancel_user_data = nullptr,
1219 .vita49_telemetry_callback = nullptr,
1220 .vita49_telemetry_user_data = nullptr,
1221 .function_name = "fers_run_simulation",
1222 .invalid_context_message = "Invalid context provided to fers_run_simulation."});
1223}
1224
1226 void* progress_user_data, fers_cancel_callback_t cancel_callback, void* cancel_user_data,
1227 fers_vita49_telemetry_callback_t vita49_telemetry_callback, void* vita49_telemetry_user_data)
1228{
1229 return run_simulation_common(
1230 SimulationRunRequest{.context = context,
1231 .progress_callback = progress_callback,
1232 .progress_user_data = progress_user_data,
1233 .cancel_callback = cancel_callback,
1234 .cancel_user_data = cancel_user_data,
1235 .vita49_telemetry_callback = vita49_telemetry_callback,
1236 .vita49_telemetry_user_data = vita49_telemetry_user_data,
1237 .function_name = "fers_run_simulation_ex",
1238 .invalid_context_message = "Invalid context provided to fers_run_simulation_ex."});
1239}
1240
1241int fers_generate_kml(const fers_context_t* context, const char* output_kml_filepath)
1242{
1243 last_error_message.clear();
1244 if ((context == nullptr) || (output_kml_filepath == nullptr))
1245 {
1246 last_error_message = "Invalid arguments: context or output_kml_filepath is NULL.";
1248 return -1;
1249 }
1250
1251 const auto* ctx = context;
1252
1253 try
1254 {
1255 const auto result = serial::KmlGenerator::generateKml(*ctx->getWorld(), output_kml_filepath);
1256 if (result)
1257 {
1258 return 0; // Success
1259 }
1260
1261 last_error_message = result.error();
1263 return 2; // Generation failed
1264 }
1265 catch (const std::exception& e)
1266 {
1267 handle_api_exception(e, "fers_generate_kml");
1268 return 1; // Exception thrown
1269 }
1270}
1271
1272// --- Helper to convert C-API enum to C++ enum ---
1274{
1275 switch (type)
1276 {
1277 case FERS_INTERP_LINEAR:
1279 case FERS_INTERP_CUBIC:
1281 case FERS_INTERP_STATIC:
1282 default:
1284 }
1285}
1286
1300
1301
1303 const size_t waypoint_count,
1304 const fers_interp_type_t interp_type,
1305 const size_t num_points)
1306{
1307 last_error_message.clear();
1308 if ((waypoints == nullptr) || waypoint_count == 0 || num_points == 0)
1309 {
1310 last_error_message = "Invalid arguments: waypoints cannot be null and counts must be > 0.";
1312 return nullptr;
1313 }
1314 if (interp_type == FERS_INTERP_CUBIC && waypoint_count < 2)
1315 {
1316 last_error_message = "Cubic interpolation requires at least 2 waypoints.";
1318 return nullptr;
1319 }
1320
1321 try
1322 {
1323 math::Path path;
1324 path.setInterp(to_cpp_interp_type(interp_type));
1325
1326 for (size_t i = 0; i < waypoint_count; ++i)
1327 {
1328 math::Coord c;
1329 c.t = waypoints[i].time;
1330 c.pos.x = waypoints[i].x;
1331 c.pos.y = waypoints[i].y;
1332 c.pos.z = waypoints[i].z;
1333 path.addCoord(c);
1334 }
1335
1336 path.finalize();
1337
1338 // NOLINTNEXTLINE(cppcoreguidelines-owning-memory): Public C API returns an owned path struct.
1339 auto* result_path = new fers_interpolated_path_t();
1340 // NOLINTNEXTLINE(cppcoreguidelines-owning-memory): Public C API frees this array with the parent path.
1341 result_path->points = new fers_interpolated_point_t[num_points];
1342 result_path->count = num_points;
1343
1344 const double start_time = waypoints[0].time;
1345 const double end_time = waypoints[waypoint_count - 1].time;
1346 const double duration = end_time - start_time;
1347
1348 // Handle static case separately
1349 if (waypoint_count < 2 || duration <= 0)
1350 {
1351 const math::Vec3 pos = path.getPosition(start_time);
1352 for (size_t i = 0; i < num_points; ++i)
1353 {
1354 result_path->points[i] = {pos.x, pos.y, pos.z, 0.0, 0.0, 0.0};
1355 }
1356 return result_path;
1357 }
1358
1359 const double time_step =
1360 duration / static_cast<double>(num_points > 1 ? num_points - 1 : static_cast<size_t>(1));
1361
1362 for (size_t i = 0; i < num_points; ++i)
1363 {
1364 const double t = start_time + static_cast<double>(i) * time_step;
1365 const math::Vec3 pos = path.getPosition(t);
1366 const math::Vec3 vel = path.getVelocity(t);
1367 result_path->points[i] = {pos.x, pos.y, pos.z, vel.x, vel.y, vel.z};
1368 }
1369
1370 return result_path;
1371 }
1372 catch (const std::exception& e)
1373 {
1374 handle_api_exception(e, "fers_get_interpolated_motion_path");
1375 return nullptr;
1376 }
1377}
1378
1380{
1381 if (path != nullptr)
1382 {
1383 // NOLINTNEXTLINE(cppcoreguidelines-owning-memory): Frees arrays owned by C API path structs.
1384 delete[] path->points;
1385 // NOLINTNEXTLINE(cppcoreguidelines-owning-memory): Frees structs allocated by
1386 // `fers_get_interpolated_motion_path`.
1387 delete path;
1388 }
1389}
1390
1392 const size_t waypoint_count,
1393 const fers_interp_type_t interp_type,
1394 const fers_angle_unit_t angle_unit,
1395 const size_t num_points)
1396{
1397 last_error_message.clear();
1398 last_warning_messages.clear();
1400 if ((waypoints == nullptr) || waypoint_count == 0 || num_points == 0)
1401 {
1402 last_error_message = "Invalid arguments: waypoints cannot be null and counts must be > 0.";
1404 return nullptr;
1405 }
1406 if (interp_type == FERS_INTERP_CUBIC && waypoint_count < 2)
1407 {
1408 last_error_message = "Cubic interpolation requires at least 2 waypoints.";
1410 return nullptr;
1411 }
1412
1413 try
1414 {
1415 const auto unit =
1417 math::RotationPath path;
1418 path.setInterp(to_cpp_rot_interp_type(interp_type));
1419
1420 for (size_t i = 0; i < waypoint_count; ++i)
1421 {
1423 waypoints[i].azimuth, unit, serial::rotation_warning_utils::ValueKind::Angle, "C-API",
1424 std::format("rotation waypoint {}", i), "azimuth");
1426 waypoints[i].elevation, unit, serial::rotation_warning_utils::ValueKind::Angle, "C-API",
1427 std::format("rotation waypoint {}", i), "elevation");
1429 waypoints[i].azimuth, waypoints[i].elevation, waypoints[i].time, unit));
1430 }
1431
1432 path.finalize();
1433
1434 // NOLINTNEXTLINE(cppcoreguidelines-owning-memory): Public C API returns an owned path struct.
1435 auto* result_path = new fers_interpolated_rotation_path_t();
1436 // NOLINTNEXTLINE(cppcoreguidelines-owning-memory): Public C API frees this array with the parent path.
1437 result_path->points = new fers_interpolated_rotation_point_t[num_points];
1438 result_path->count = num_points;
1439
1440 const double start_time = waypoints[0].time;
1441 const double end_time = waypoints[waypoint_count - 1].time;
1442 const double duration = end_time - start_time;
1443
1444 // Handle static case separately
1445 if (waypoint_count < 2 || duration <= 0)
1446 {
1447 const math::SVec3 rot = path.getPosition(start_time);
1448 for (size_t i = 0; i < num_points; ++i)
1449 {
1450 result_path->points[i] = fers_interpolated_rotation_point_t{
1453 }
1454 return result_path;
1455 }
1456
1457 const double time_step =
1458 duration / static_cast<double>(num_points > 1 ? num_points - 1 : static_cast<size_t>(1));
1459
1460 for (size_t i = 0; i < num_points; ++i)
1461 {
1462 const double t = start_time + static_cast<double>(i) * time_step;
1463 const math::SVec3 rot = path.getPosition(t);
1464
1465 result_path->points[i] = fers_interpolated_rotation_point_t{
1468 }
1469
1470 return result_path;
1471 }
1472 catch (const std::exception& e)
1473 {
1475 handle_api_exception(e, "fers_get_interpolated_rotation_path");
1476 return nullptr;
1477 }
1478}
1479
1481{
1482 if (path != nullptr)
1483 {
1484 // NOLINTNEXTLINE(cppcoreguidelines-owning-memory): Frees arrays owned by C API path structs.
1485 delete[] path->points;
1486 // NOLINTNEXTLINE(cppcoreguidelines-owning-memory): Frees structs allocated by
1487 // `fers_get_interpolated_rotation_path`.
1488 delete path;
1489 }
1490}
1491
1492// --- Antenna Pattern Implementation ---
1493
1495 const size_t az_samples, const size_t el_samples,
1496 const double frequency_hz)
1497{
1498 last_error_message.clear();
1499 if ((context == nullptr) || az_samples < 2 || el_samples < 2)
1500 {
1501 last_error_message = "Invalid arguments: context must be non-null and sample counts must be >= 2.";
1503 return nullptr;
1504 }
1505
1506 try
1507 {
1508 const auto* ctx = context;
1509 antenna::Antenna const* ant = ctx->getWorld()->findAntenna(static_cast<SimId>(antenna_id));
1510
1511 if (ant == nullptr)
1512 {
1513 last_error_message = "Antenna ID '" + std::to_string(antenna_id) + "' not found in the world.";
1515 return nullptr;
1516 }
1517
1518 // TODO: Currently only using the first-found waveform. This is incorrect but also difficult to represent
1519 // correctly in scenarios with multiple waveforms as the gain for squarehorn and parabolic antennas
1520 // depends on the wavelength. Hence a decision needs to be made about whether to return multiple patterns
1521 // per waveform or have the user specify a representative wavelength in the UI per antenna.
1522 // Calculate wavelength from the provided frequency.
1523 // Default to 1GHz (0.3m) if frequency is invalid/zero, though the UI should prevent this
1524 // for antennas that strictly require it (Horn/Parabolic).
1525 RealType wavelength = 0.3;
1526 if (frequency_hz > 0.0)
1527 {
1528 wavelength = params::c() / frequency_hz;
1529 }
1530
1531 // NOLINTNEXTLINE(cppcoreguidelines-owning-memory): Public C API returns owned antenna pattern data.
1532 auto* data = new fers_antenna_pattern_data_t();
1533 data->az_count = az_samples;
1534 data->el_count = el_samples;
1535 const size_t total_samples = az_samples * el_samples;
1536 // NOLINTNEXTLINE(cppcoreguidelines-owning-memory): Public C API frees this array with the parent data.
1537 data->gains = new double[total_samples];
1538
1539 // The reference angle (boresight) is implicitly the local X-axis in the FERS engine.
1540 // We pass a zero rotation to get the gain relative to this boresight.
1541 const math::SVec3 ref_angle(1.0, 0.0, 0.0);
1542 double max_gain = 0.0;
1543
1544 const auto az_denominator = static_cast<RealType>(az_samples - 1);
1545 const auto el_denominator = static_cast<RealType>(el_samples - 1);
1546
1547 for (size_t i = 0; i < el_samples; ++i)
1548 {
1549 // Elevation from -PI/2 to PI/2
1550 const RealType elevation = (static_cast<RealType>(i) / el_denominator) * PI - (PI / 2.0);
1551 for (size_t j = 0; j < az_samples; ++j)
1552 {
1553 // Azimuth from -PI to PI
1554 const RealType azimuth = (static_cast<RealType>(j) / az_denominator) * 2.0 * PI - PI;
1555 const math::SVec3 sample_angle(1.0, azimuth, elevation);
1556 const RealType gain = ant->getGain(sample_angle, ref_angle, wavelength);
1557 data->gains[i * az_samples + j] = gain;
1558 max_gain = std::max(gain, max_gain);
1559 }
1560 }
1561
1562 data->max_gain = max_gain;
1563
1564 // Normalize the gains
1565 if (max_gain > 0)
1566 {
1567 for (size_t i = 0; i < total_samples; ++i)
1568 {
1569 data->gains[i] /= max_gain;
1570 }
1571 }
1572
1573 return data;
1574 }
1575 catch (const std::exception& e)
1576 {
1577 handle_api_exception(e, "fers_get_antenna_pattern");
1578 return nullptr;
1579 }
1580}
1581
1583{
1584 if (data != nullptr)
1585 {
1586 // NOLINTNEXTLINE(cppcoreguidelines-owning-memory): Frees arrays owned by C API pattern structs.
1587 delete[] data->gains;
1588 // NOLINTNEXTLINE(cppcoreguidelines-owning-memory): Frees structs allocated by `fers_get_antenna_pattern`.
1589 delete data;
1590 }
1591}
1592
1593// --- Preview Link Calculation Implementation ---
1594
1596{
1597 last_error_message.clear();
1598 if (context == nullptr)
1599 {
1600 last_error_message = "Invalid context passed to fers_calculate_preview_links";
1602 return nullptr;
1603 }
1604
1605 try
1606 {
1607 const auto* ctx = context;
1608 // Call the core physics logic in channel_model.cpp
1609 const auto cpp_links = simulation::calculatePreviewLinks(*ctx->getWorld(), time);
1610
1611 // Convert C++ vector to C-API struct
1612 // NOLINTNEXTLINE(cppcoreguidelines-owning-memory): Public C API returns owned preview-link lists.
1613 auto* result = new fers_visual_link_list_t();
1614 result->count = cpp_links.size();
1615
1616 if (!cpp_links.empty())
1617 {
1618 // NOLINTNEXTLINE(cppcoreguidelines-owning-memory): Public C API frees this array with the parent list.
1619 result->links = new fers_visual_link_t[result->count];
1620 for (size_t i = 0; i < result->count; ++i)
1621 {
1622 const auto& src = cpp_links[i];
1623 auto& dst = result->links[i];
1624
1625 // Map enums
1626 switch (src.type)
1627 {
1630 break;
1632 dst.type = FERS_LINK_BISTATIC_TX_TGT;
1633 break;
1635 dst.type = FERS_LINK_BISTATIC_TGT_RX;
1636 break;
1638 dst.type = FERS_LINK_DIRECT_TX_RX;
1639 break;
1640 }
1641
1642 dst.quality = (src.quality == simulation::LinkQuality::Strong) ? FERS_LINK_STRONG : FERS_LINK_WEAK;
1643
1644 copy_visual_link_label(dst, src.label);
1645
1646 dst.source_id = static_cast<uint64_t>(src.source_id);
1647 dst.dest_id = static_cast<uint64_t>(src.dest_id);
1648 dst.origin_id = static_cast<uint64_t>(src.origin_id);
1649 dst.rcs = src.rcs;
1650 dst.actual_power_dbm = src.actual_power_dbm;
1651 dst.display_value = src.display_value;
1652 }
1653 }
1654 else
1655 {
1656 result->links = nullptr;
1657 }
1658 return result;
1659 }
1660 catch (const std::exception& e)
1661 {
1662 handle_api_exception(e, "fers_calculate_preview_links");
1663 return nullptr;
1664 }
1665}
1666
1668{
1669 if (list != nullptr)
1670 {
1671 // NOLINTNEXTLINE(cppcoreguidelines-owning-memory): Frees arrays owned by C API preview-link lists.
1672 delete[] list->links;
1673 // NOLINTNEXTLINE(cppcoreguidelines-owning-memory): Frees structs allocated by `fers_calculate_preview_links`.
1674 delete list;
1675 }
1676}
1677}
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:728
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:867
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:1225
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:1582
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:656
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:392
char * fers_get_last_warning_messages_json()
Returns the last deduplicated rotation-unit warning list for the calling thread as JSON.
Definition api.cpp:1097
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:893
int fers_set_vita49_queue_depth(fers_context_t *context, const std::uint32_t queue_depth)
Definition api.cpp:568
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:916
int fers_use_hdf5_output(fers_context_t *context)
Resets the context output mode to the default HDF5 output.
Definition api.cpp:440
char * fers_get_last_error_message()
Retrieves the last error message that occurred on the current thread.
Definition api.cpp:1084
int fers_enable_vita49_udp_output(fers_context_t *context, const char *host, const std::uint16_t port)
Definition api.cpp:464
void fers_free_interpolated_motion_path(fers_interpolated_path_t *path)
Frees the memory allocated for an interpolated motion path.
Definition api.cpp:1379
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:842
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:1241
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:781
int fers_set_output_directory(fers_context_t *context, const char *out_dir)
Sets the output directory for simulation results.
Definition api.cpp:419
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:966
void fers_free_preview_links(fers_visual_link_list_t *list)
Frees the memory allocated for a preview link list.
Definition api.cpp:1667
int fers_set_vita49_max_udp_payload(fers_context_t *context, const std::uint16_t max_udp_payload)
Definition api.cpp:547
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:941
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:1302
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:1211
void fers_free_string(char *str)
Frees a string that was allocated and returned by the libfers API.
Definition api.cpp:1109
fers_log_level_t fers_get_log_level()
Returns the current internal logger level.
Definition api.cpp:379
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:1287
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:1494
const char * fers_get_version(void)
Returns the library version string.
Definition api.cpp:377
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:353
void fers_free_interpolated_rotation_path(fers_interpolated_rotation_path_t *path)
Frees the memory allocated for an interpolated rotation path.
Definition api.cpp:1480
math::Path::InterpType to_cpp_interp_type(const fers_interp_type_t type)
Definition api.cpp:1273
void fers_set_log_callback(fers_log_callback_t callback, void *user_data)
Registers a callback for formatted log lines.
Definition api.cpp:381
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:605
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:1595
char * fers_get_scenario_as_json(fers_context_t *context)
Serializes the current simulation scenario into a JSON string.
Definition api.cpp:701
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:589
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:991
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:806
int fers_set_vita49_epoch_unix_nanoseconds(fers_context_t *context, const std::uint64_t epoch_unix_nanoseconds)
Definition api.cpp:526
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:1021
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:1391
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:1045
char * fers_get_last_output_metadata_json(fers_context_t *context)
Returns JSON metadata for the most recent simulation output files.
Definition api.cpp:758
int fers_set_thread_count(unsigned num_threads)
Sets the number of worker threads for the simulation.
Definition api.cpp:400
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:505
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:273
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::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.