Refactor radar system

This commit is contained in:
Ayzen
2026-10-05 16:46:28 +03:00
parent 515049d2ed
commit 8f097dfa2e
179 changed files with 8212 additions and 6676 deletions
@@ -32,10 +32,7 @@ struct RadarConfig {
// Radar device identity and runtime mode.
std::string model = "librevna";
std::string serial{};
std::string remote_host = "127.0.0.1";
std::uint32_t remote_port = 50209;
DriverMode driver_mode = DriverMode::Mock;
float mock_signal_hz = 5'000'000.0F;
RadarSweepSettings sweep{};
};
@@ -134,7 +131,7 @@ struct GprRxGeometry {
};
struct GprConfig {
// Stable collection-level GPR processing settings.
// Initial GPR settings; the data_processor can override them through live config.
float relative_permittivity = 1.0F;
std::vector<GprTxGeometry> tx_geometry{};
std::vector<GprRxGeometry> rx_geometry{};
@@ -154,5 +151,6 @@ struct RunConfig {
};
[[nodiscard]] auto load_run_config(const std::string& path) -> RunConfig;
void validate_gpr_config(const GprConfig& config, const RunConfig& run_config);
} // namespace radar::config
@@ -0,0 +1,20 @@
#pragma once
#include <algorithm>
#include <atomic>
#include <chrono>
#include <cstdint>
#include <thread>
namespace radar::config {
// Returns true when stop interrupts the delay, checking at least every 50 ms.
inline auto wait_or_stop(std::uint32_t delay_ms, const std::atomic<bool>& stop) -> bool {
std::uint32_t waited = 0;
while (waited < delay_ms && !stop.load(std::memory_order_relaxed)) {
const auto chunk = std::min(50U, delay_ms - waited);
std::this_thread::sleep_for(std::chrono::milliseconds(chunk));
waited += chunk;
}
return stop.load(std::memory_order_relaxed);
}
} // namespace radar::config
@@ -65,7 +65,18 @@ using Json = nlohmann::json;
if (!value.is_number()) {
throw std::runtime_error("Expected JSON number at " + context);
}
return value.get<double>();
const auto numeric = value.get<double>();
if (!std::isfinite(numeric)) {
throw std::runtime_error("Expected finite number at " + context);
}
return numeric;
}
[[nodiscard]] auto number_to_f32(double value, const std::string& context) -> float {
if (!std::isfinite(value) || std::fabs(value) > std::numeric_limits<float>::max()) {
throw std::runtime_error("Value out of finite float32 range at " + context);
}
return static_cast<float>(value);
}
[[nodiscard]] auto number_to_u32(double value, const std::string& context) -> std::uint32_t {
@@ -74,7 +85,7 @@ using Json = nlohmann::json;
}
const auto rounded = std::round(value);
if (std::fabs(value - rounded) > 0.000001) {
if (value != rounded) {
throw std::runtime_error("Expected integer value at " + context);
}
return static_cast<std::uint32_t>(rounded);
@@ -82,7 +93,7 @@ using Json = nlohmann::json;
[[nodiscard]] auto number_to_i32(double value, const std::string& context) -> std::int32_t {
const auto rounded = std::round(value);
if (std::fabs(value - rounded) > 0.000001) {
if (value != rounded) {
throw std::runtime_error("Expected integer value at " + context);
}
@@ -135,7 +146,7 @@ using Json = nlohmann::json;
[[nodiscard]] auto optional_f32(const Json& object, const std::string& key, float fallback) -> float {
if (const auto* value = optional_field(object, key); value != nullptr) {
return static_cast<float>(as_number(*value, key));
return number_to_f32(as_number(*value, key), key);
}
return fallback;
}
@@ -173,8 +184,8 @@ using Json = nlohmann::json;
}
PreprocessNotchBandConfig band{};
band.low_hz = static_cast<float>(as_number((*band_array)[0], context + ".bands_hz[][0]"));
band.high_hz = static_cast<float>(as_number((*band_array)[1], context + ".bands_hz[][1]"));
band.low_hz = number_to_f32(as_number((*band_array)[0], context + ".bands_hz[][0]"), context);
band.high_hz = number_to_f32(as_number((*band_array)[1], context + ".bands_hz[][1]"), context);
notch.bands_hz.push_back(band);
}
}
@@ -242,6 +253,9 @@ using Json = nlohmann::json;
}
void validate_radar_sweep(const RadarSweepSettings& sweep) {
if (sweep.start_hz < 0.0F || sweep.stop_hz < 0.0F || !(sweep.if_bandwidth_hz > 0.0F)) {
throw std::runtime_error("radar.sweep frequencies must be non-negative and IF bandwidth must be > 0");
}
if (sweep.points == 0U) {
throw std::runtime_error("radar.sweep.points must be > 0");
}
@@ -297,32 +311,6 @@ void validate_combo_key(const ipc::ComboKey& key, const RunConfig& config) {
}
}
void validate_gpr_config(const GprConfig& config, const RunConfig& run_config) {
if (!(config.relative_permittivity > 0.0F)) {
throw std::runtime_error("gpr.relative_permittivity must be > 0");
}
std::unordered_set<std::uint32_t> seen_tx_positions{};
for (const auto& entry : config.tx_geometry) {
if (entry.output_pos >= run_config.output_switch.positions) {
throw std::runtime_error("gpr.tx_geometry output_pos is out of range");
}
if (!seen_tx_positions.insert(entry.output_pos).second) {
throw std::runtime_error("gpr.tx_geometry contains duplicate output_pos");
}
}
std::unordered_set<std::uint32_t> seen_rx_positions{};
for (const auto& entry : config.rx_geometry) {
if (entry.input_pos >= run_config.input_switch.positions) {
throw std::runtime_error("gpr.rx_geometry input_pos is out of range");
}
if (!seen_rx_positions.insert(entry.input_pos).second) {
throw std::runtime_error("gpr.rx_geometry contains duplicate input_pos");
}
}
}
[[nodiscard]] auto parse_gpr_config(const Json& object, const RunConfig& run_config) -> GprConfig {
const auto* gpr_obj = as_object(object, "gpr");
GprConfig config{};
@@ -406,6 +394,9 @@ void validate_gpr_config(const GprConfig& config, const RunConfig& run_config) {
if (endpoint.slot_size_bytes == 0U) {
throw std::runtime_error("Ring slot_size_bytes must be > 0 for " + key);
}
if (endpoint.slot_size_bytes % 8U != 0U) {
throw std::runtime_error("Ring slot_size_bytes must be a multiple of 8 for " + key);
}
if (endpoint.name.empty() || endpoint.name.front() != '/') {
throw std::runtime_error("Ring name must start with '/' for " + key);
}
@@ -415,6 +406,39 @@ void validate_gpr_config(const GprConfig& config, const RunConfig& run_config) {
} // namespace
void validate_gpr_config(const GprConfig& config, const RunConfig& run_config) {
if (!std::isfinite(config.relative_permittivity) || !(config.relative_permittivity > 0.0F)) {
throw std::runtime_error("gpr.relative_permittivity must be > 0");
}
std::unordered_set<std::uint32_t> seen_tx_positions{};
for (const auto& entry : config.tx_geometry) {
if (!std::isfinite(entry.x_m) || !std::isfinite(entry.y_m) || !std::isfinite(entry.z_m)) {
throw std::runtime_error("gpr.tx_geometry coordinates must be finite");
}
if (entry.output_pos >= run_config.output_switch.positions) {
throw std::runtime_error("gpr.tx_geometry output_pos is out of range");
}
if (!seen_tx_positions.insert(entry.output_pos).second) {
throw std::runtime_error("gpr.tx_geometry contains duplicate output_pos");
}
}
std::unordered_set<std::uint32_t> seen_rx_positions{};
for (const auto& entry : config.rx_geometry) {
if (!std::isfinite(entry.x_m) || !std::isfinite(entry.y_m) || !std::isfinite(entry.z_m)) {
throw std::runtime_error("gpr.rx_geometry coordinates must be finite");
}
if (entry.input_pos >= run_config.input_switch.positions) {
throw std::runtime_error("gpr.rx_geometry input_pos is out of range");
}
if (!seen_rx_positions.insert(entry.input_pos).second) {
throw std::runtime_error("gpr.rx_geometry contains duplicate input_pos");
}
}
}
auto load_run_config(const std::string& path) -> RunConfig {
std::ifstream stream(path);
if (!stream.is_open()) {
@@ -440,30 +464,21 @@ auto load_run_config(const std::string& path) -> RunConfig {
const auto* radar_obj = as_object(required_field(*root_obj, "radar"), "radar");
config.radar.model = optional_string(*radar_obj, "model", "librevna");
config.radar.serial = optional_string(*radar_obj, "serial", "");
config.radar.remote_host = optional_string(*radar_obj, "remote_host", "127.0.0.1");
config.radar.remote_port = optional_u32(*radar_obj, "remote_port", 50209);
if (config.radar.remote_host.empty()) {
throw std::runtime_error("radar.remote_host must not be empty");
}
if (config.radar.remote_port == 0U || config.radar.remote_port > 65535U) {
throw std::runtime_error("radar.remote_port must be in 1..65535");
}
config.radar.driver_mode = parse_driver_mode(optional_string(*radar_obj, "driver_mode", "mock"));
config.radar.mock_signal_hz = optional_f32(*radar_obj, "mock_signal_hz", 5'000'000.0F);
const auto* sweep_obj = as_object(required_field(*radar_obj, "sweep"), "radar.sweep");
config.radar.sweep.start_hz =
static_cast<float>(as_number(required_field(*sweep_obj, "start_hz"), "radar.sweep.start_hz"));
number_to_f32(as_number(required_field(*sweep_obj, "start_hz"), "radar.sweep.start_hz"), "radar.sweep.start_hz");
config.radar.sweep.stop_hz =
static_cast<float>(as_number(required_field(*sweep_obj, "stop_hz"), "radar.sweep.stop_hz"));
number_to_f32(as_number(required_field(*sweep_obj, "stop_hz"), "radar.sweep.stop_hz"), "radar.sweep.stop_hz");
if (const auto* points_value = optional_field(*sweep_obj, "points"); points_value != nullptr) {
config.radar.sweep.points =
number_to_u32(as_number(*points_value, "radar.sweep.points"), "radar.sweep.points");
} else if (config.radar.model != "kamil_adc") {
throw std::runtime_error("Missing required config field: points");
}
config.radar.sweep.if_bandwidth_hz = static_cast<float>(
as_number(required_field(*sweep_obj, "if_bandwidth_hz"), "radar.sweep.if_bandwidth_hz")
config.radar.sweep.if_bandwidth_hz = number_to_f32(
as_number(required_field(*sweep_obj, "if_bandwidth_hz"), "radar.sweep.if_bandwidth_hz"), "radar.sweep.if_bandwidth_hz"
);
config.radar.sweep.power_dbm = optional_f32(*sweep_obj, "stimulus_power_dbm", -10.0F);
validate_radar_sweep(config.radar.sweep);