init commit

This commit is contained in:
Ayzen
2026-03-05 14:42:33 +03:00
commit fd4618b20d
964 changed files with 325114 additions and 0 deletions
@@ -0,0 +1,97 @@
#pragma once
#include <cstdint>
#include <string>
#include <vector>
#include "shared_types.hpp"
namespace radar::config {
enum class DriverMode {
Mock,
Native,
};
enum class SwitchDriverKind {
H7992,
HMC349A,
};
struct RadarSweepSettings {
// Frequency sweep bounds and acquisition settings.
float start_hz = 1'000'000.0F;
float stop_hz = 6'000'000'000.0F;
std::uint32_t points = 501;
float if_bandwidth_hz = 1'000.0F;
float power_dbm = -10.0F;
};
struct RadarConfig {
// Radar device identity and runtime mode.
std::string model = "librevna";
std::string serial{};
DriverMode driver_mode = DriverMode::Mock;
float mock_signal_hz = 5'000'000.0F;
RadarSweepSettings sweep{};
};
struct SwitchConfig {
// One RF switch attached to a specific radar port.
std::string name{};
DriverMode driver_mode = DriverMode::Mock;
SwitchDriverKind driver_kind = SwitchDriverKind::H7992;
std::uint32_t radar_port = 0;
std::uint32_t positions = 4;
std::uint32_t default_position = 0;
std::string gpio_chip = "/dev/gpiochip0";
std::int32_t pin_a = 17;
std::int32_t pin_b = 27;
bool invert_logic = false;
};
struct RingEndpointConfig {
// Shared-memory endpoint geometry.
std::string name{};
std::uint32_t capacity = 32;
std::uint32_t slot_size_bytes = 2U * 1024U * 1024U;
};
struct RingsConfig {
RingEndpointConfig raw{.name = "/radar_raw"};
RingEndpointConfig raw_tap{.name = "/radar_raw_tap"};
RingEndpointConfig preprocessed{.name = "/radar_preprocessed"};
RingEndpointConfig preprocessed_tap{.name = "/radar_preprocessed_tap"};
RingEndpointConfig results{.name = "/radar_results"};
};
struct RuntimeConfig {
// Runtime pacing and behavior flags for C++ workers.
std::uint32_t settling_ms = 0;
std::uint32_t idle_sleep_ms = 2;
bool continuous = true;
std::string processing_live_config_path = "python_app/runtime/processing_live.json";
};
struct PreprocessConfig {
// Names and bundle paths selected by Python GUI layer.
std::string calibration_set{};
std::string reference_set{};
std::string calibration_bundle_path{};
std::string reference_bundle_path{};
};
struct RunConfig {
// Single configuration object shared by all C++ processes.
RadarConfig radar{};
SwitchConfig input_switch{};
SwitchConfig output_switch{};
RingsConfig rings{};
RuntimeConfig runtime{};
PreprocessConfig preprocess{};
std::vector<radar::ipc::ComboKey> run_combos{};
};
[[nodiscard]] auto load_run_config(const std::string& path) -> RunConfig;
} // namespace radar::config
@@ -0,0 +1,398 @@
#include "run_config.hpp"
#include <cmath>
#include <cstdint>
#include <fstream>
#include <iterator>
#include <limits>
#include <stdexcept>
#include <string>
#include <nlohmann/json.hpp>
namespace radar::config {
namespace {
using Json = nlohmann::json;
[[nodiscard]] auto required_field(const Json& object, const std::string& key) -> const Json& {
const auto found = object.find(key);
if (found == object.end()) {
throw std::runtime_error("Missing required config field: " + key);
}
return *found;
}
[[nodiscard]] auto optional_field(const Json& object, const std::string& key) -> const Json* {
const auto found = object.find(key);
if (found == object.end()) {
return nullptr;
}
return &(*found);
}
[[nodiscard]] auto as_object(const Json& value, const std::string& context) -> const Json* {
if (!value.is_object()) {
throw std::runtime_error("Expected JSON object at " + context);
}
return &value;
}
[[nodiscard]] auto as_array(const Json& value, const std::string& context) -> const Json* {
if (!value.is_array()) {
throw std::runtime_error("Expected JSON array at " + context);
}
return &value;
}
[[nodiscard]] auto as_string(const Json& value, const std::string& context) -> const std::string& {
if (!value.is_string()) {
throw std::runtime_error("Expected JSON string at " + context);
}
return value.get_ref<const std::string&>();
}
[[nodiscard]] auto as_bool(const Json& value, const std::string& context) -> bool {
if (!value.is_boolean()) {
throw std::runtime_error("Expected JSON bool at " + context);
}
return value.get<bool>();
}
[[nodiscard]] auto as_number(const Json& value, const std::string& context) -> double {
if (!value.is_number()) {
throw std::runtime_error("Expected JSON number at " + context);
}
return value.get<double>();
}
[[nodiscard]] auto number_to_u32(double value, const std::string& context) -> std::uint32_t {
if (value < 0.0 || value > static_cast<double>(std::numeric_limits<std::uint32_t>::max())) {
throw std::runtime_error("Value out of uint32 range at " + context);
}
const auto rounded = std::round(value);
if (std::fabs(value - rounded) > 0.000001) {
throw std::runtime_error("Expected integer value at " + context);
}
return static_cast<std::uint32_t>(rounded);
}
[[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) {
throw std::runtime_error("Expected integer value at " + context);
}
if (rounded < static_cast<double>(std::numeric_limits<std::int32_t>::min()) ||
rounded > static_cast<double>(std::numeric_limits<std::int32_t>::max())) {
throw std::runtime_error("Value out of int32 range at " + context);
}
return static_cast<std::int32_t>(rounded);
}
[[nodiscard]] auto optional_string(
const Json& object,
const std::string& key,
const std::string& fallback
) -> std::string {
if (const auto* value = optional_field(object, key); value != nullptr) {
return as_string(*value, key);
}
return fallback;
}
[[nodiscard]] auto optional_bool(const Json& object, const std::string& key, bool fallback) -> bool {
if (const auto* value = optional_field(object, key); value != nullptr) {
return as_bool(*value, key);
}
return fallback;
}
[[nodiscard]] auto optional_u32(
const Json& object,
const std::string& key,
std::uint32_t fallback
) -> std::uint32_t {
if (const auto* value = optional_field(object, key); value != nullptr) {
return number_to_u32(as_number(*value, key), key);
}
return fallback;
}
[[nodiscard]] auto optional_i32(
const Json& object,
const std::string& key,
std::int32_t fallback
) -> std::int32_t {
if (const auto* value = optional_field(object, key); value != nullptr) {
return number_to_i32(as_number(*value, key), key);
}
return fallback;
}
[[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 fallback;
}
[[nodiscard]] auto parse_driver_mode(const std::string& value) -> DriverMode {
if (value == "mock") {
return DriverMode::Mock;
}
if (value == "native") {
return DriverMode::Native;
}
throw std::runtime_error("Unsupported driver_mode value: " + value);
}
[[nodiscard]] auto parse_switch_driver_kind(const std::string& value) -> SwitchDriverKind {
if (value == "h7992") {
return SwitchDriverKind::H7992;
}
if (value == "hmc349a") {
return SwitchDriverKind::HMC349A;
}
throw std::runtime_error("Unsupported switch driver value: " + value);
}
void validate_radar_sweep(const RadarSweepSettings& sweep) {
if (sweep.points == 0U) {
throw std::runtime_error("radar.sweep.points must be > 0");
}
if (sweep.stop_hz < sweep.start_hz) {
throw std::runtime_error("radar.sweep.stop_hz must be >= start_hz");
}
}
void validate_switch_config(const SwitchConfig& config) {
if (config.radar_port != 1U && config.radar_port != 2U) {
throw std::runtime_error("Switch radar_port must be 1 or 2 for " + config.name);
}
if (config.positions == 0U) {
throw std::runtime_error("Switch positions must be > 0 for " + config.name);
}
if (config.default_position >= config.positions) {
throw std::runtime_error("Switch default_position is out of range for " + config.name);
}
if (config.driver_mode != DriverMode::Native) {
return;
}
if (config.gpio_chip.empty()) {
throw std::runtime_error("gpio_chip is required for native switch mode: " + config.name);
}
if (config.driver_kind == SwitchDriverKind::H7992) {
if (config.positions > 4U) {
throw std::runtime_error("Native H7992 switch driver supports positions <= 4 for " + config.name);
}
if (config.pin_a < 0 || config.pin_b < 0 || config.pin_a == config.pin_b) {
throw std::runtime_error("pin_a/pin_b are invalid for native H7992 mode: " + config.name);
}
return;
}
if (config.positions > 2U) {
throw std::runtime_error("Native HMC349A switch driver supports positions <= 2 for " + config.name);
}
if (config.pin_a < 0) {
throw std::runtime_error("pin_a (control) is required for native HMC349A mode: " + config.name);
}
}
void validate_combo_key(const ipc::ComboKey& key, const RunConfig& config) {
if (key.input_pos >= config.input_switch.positions) {
throw std::runtime_error("run.combos input index out of range");
}
if (key.output_pos >= config.output_switch.positions) {
throw std::runtime_error("run.combos output index out of range");
}
}
[[nodiscard]] auto parse_switch_config(
const Json& object,
const std::string& default_name,
std::uint32_t default_radar_port
) -> SwitchConfig {
const auto* switch_obj = as_object(object, default_name);
SwitchConfig config{};
config.name = optional_string(*switch_obj, "name", default_name);
config.driver_mode = parse_driver_mode(optional_string(*switch_obj, "driver_mode", "mock"));
config.driver_kind = parse_switch_driver_kind(optional_string(*switch_obj, "driver", "h7992"));
config.radar_port = optional_u32(*switch_obj, "radar_port", default_radar_port);
config.positions = optional_u32(*switch_obj, "positions", 4);
config.default_position = optional_u32(*switch_obj, "default_position", 0);
config.gpio_chip = optional_string(*switch_obj, "gpio_chip", "/dev/gpiochip0");
config.pin_a = optional_i32(*switch_obj, "pin_a", 17);
config.pin_b = optional_i32(*switch_obj, "pin_b", 27);
config.invert_logic = optional_bool(*switch_obj, "invert_logic", false);
validate_switch_config(config);
return config;
}
[[nodiscard]] auto parse_ring_endpoint(
const Json& object,
const std::string& key,
const RingEndpointConfig& fallback
) -> RingEndpointConfig {
RingEndpointConfig endpoint = fallback;
const auto* value = optional_field(object, key);
if (value == nullptr) {
return endpoint;
}
const auto* endpoint_obj = as_object(*value, key);
endpoint.name = optional_string(*endpoint_obj, "name", endpoint.name);
endpoint.capacity = optional_u32(*endpoint_obj, "capacity", endpoint.capacity);
endpoint.slot_size_bytes = optional_u32(*endpoint_obj, "slot_size_bytes", endpoint.slot_size_bytes);
if (endpoint.capacity == 0U) {
throw std::runtime_error("Ring capacity must be > 0 for " + key);
}
if (endpoint.slot_size_bytes == 0U) {
throw std::runtime_error("Ring slot_size_bytes must be > 0 for " + key);
}
if (endpoint.name.empty() || endpoint.name.front() != '/') {
throw std::runtime_error("Ring name must start with '/' for " + key);
}
return endpoint;
}
} // namespace
auto load_run_config(const std::string& path) -> RunConfig {
std::ifstream stream(path);
if (!stream.is_open()) {
throw std::runtime_error("Failed to open config file: " + path);
}
const std::string json_text((std::istreambuf_iterator<char>(stream)), std::istreambuf_iterator<char>());
if (json_text.empty()) {
throw std::runtime_error("Config file is empty: " + path);
}
Json root{};
try {
root = Json::parse(json_text);
} catch (const Json::parse_error& error) {
throw std::runtime_error("Failed to parse config file: " + path + ": " + std::string(error.what()));
}
const auto* root_obj = as_object(root, "root");
RunConfig config{};
{
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.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"));
config.radar.sweep.stop_hz =
static_cast<float>(as_number(required_field(*sweep_obj, "stop_hz"), "radar.sweep.stop_hz"));
config.radar.sweep.points =
number_to_u32(as_number(required_field(*sweep_obj, "points"), "radar.sweep.points"), "radar.sweep.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.power_dbm = optional_f32(*sweep_obj, "stimulus_power_dbm", -10.0F);
validate_radar_sweep(config.radar.sweep);
}
{
const auto* switches_obj = as_object(required_field(*root_obj, "switches"), "switches");
if (const auto* port1_value = optional_field(*switches_obj, "port1"); port1_value != nullptr) {
const auto* port1_obj = as_object(*port1_value, "switches.port1");
const auto* port2_obj = as_object(required_field(*switches_obj, "port2"), "switches.port2");
config.output_switch = parse_switch_config(*port1_obj, "port1", 1);
config.input_switch = parse_switch_config(*port2_obj, "port2", 2);
} else {
config.input_switch = parse_switch_config(
*as_object(required_field(*switches_obj, "input"), "switches.input"),
"input",
2
);
config.output_switch = parse_switch_config(
*as_object(required_field(*switches_obj, "output"), "switches.output"),
"output",
1
);
}
if (config.input_switch.radar_port == config.output_switch.radar_port) {
throw std::runtime_error("Switch radar_port mapping must be unique for two switches");
}
}
{
const auto* run_obj = as_object(required_field(*root_obj, "run"), "run");
config.runtime.settling_ms = optional_u32(*run_obj, "settling_ms", 0);
config.runtime.idle_sleep_ms = optional_u32(*run_obj, "idle_sleep_ms", 2);
config.runtime.continuous = optional_bool(*run_obj, "continuous", true);
config.runtime.processing_live_config_path = optional_string(
*run_obj,
"processing_live_config_path",
"python_app/runtime/processing_live.json"
);
const auto* combos = as_array(required_field(*run_obj, "combos"), "run.combos");
if (combos->empty()) {
throw std::runtime_error("run.combos must not be empty");
}
config.run_combos.reserve(combos->size());
for (const auto& combo_value : *combos) {
const auto* combo_obj = as_object(combo_value, "run.combos[]");
ipc::ComboKey key{};
key.input_pos = number_to_u32(
as_number(required_field(*combo_obj, "input"), "run.combos.input"),
"run.combos.input"
);
key.output_pos = number_to_u32(
as_number(required_field(*combo_obj, "output"), "run.combos.output"),
"run.combos.output"
);
validate_combo_key(key, config);
config.run_combos.push_back(key);
}
}
{
const auto* preprocess_obj = as_object(required_field(*root_obj, "preprocess"), "preprocess");
config.preprocess.calibration_set = optional_string(*preprocess_obj, "calibration_set", "");
config.preprocess.reference_set = optional_string(*preprocess_obj, "reference_set", "");
config.preprocess.calibration_bundle_path = optional_string(*preprocess_obj, "calibration_bundle_path", "");
config.preprocess.reference_bundle_path = optional_string(*preprocess_obj, "reference_bundle_path", "");
}
{
const auto* rings_obj = as_object(required_field(*root_obj, "rings"), "rings");
config.rings.raw = parse_ring_endpoint(*rings_obj, "raw", config.rings.raw);
config.rings.raw_tap = parse_ring_endpoint(*rings_obj, "raw_tap", config.rings.raw_tap);
config.rings.preprocessed = parse_ring_endpoint(*rings_obj, "preprocessed", config.rings.preprocessed);
config.rings.preprocessed_tap = parse_ring_endpoint(
*rings_obj,
"preprocessed_tap",
config.rings.preprocessed_tap
);
config.rings.results = parse_ring_endpoint(*rings_obj, "results", config.rings.results);
}
return config;
}
} // namespace radar::config