init commit
This commit is contained in:
@@ -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
|
||||
@@ -0,0 +1,91 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace radar::ipc {
|
||||
|
||||
struct Complex32 {
|
||||
float re = 0.0F;
|
||||
float im = 0.0F;
|
||||
};
|
||||
|
||||
struct ComboKey {
|
||||
// Zero-based switch positions.
|
||||
std::uint32_t input_pos = 0;
|
||||
std::uint32_t output_pos = 0;
|
||||
|
||||
[[nodiscard]] auto operator==(const ComboKey& other) const -> bool {
|
||||
return input_pos == other.input_pos && output_pos == other.output_pos;
|
||||
}
|
||||
};
|
||||
|
||||
struct ComboKeyHash {
|
||||
[[nodiscard]] auto operator()(const ComboKey& key) const noexcept -> std::size_t {
|
||||
return (static_cast<std::size_t>(key.input_pos) << 32U) ^ static_cast<std::size_t>(key.output_pos);
|
||||
}
|
||||
};
|
||||
|
||||
struct SweepTraceBlock {
|
||||
ComboKey combo{};
|
||||
// Frequency axis in Hz. Must have the same size as `s21`.
|
||||
std::vector<float> frequency_hz{};
|
||||
// Complex S21 samples for matching frequency points.
|
||||
std::vector<Complex32> s21{};
|
||||
};
|
||||
|
||||
struct RawSweepCollection {
|
||||
std::uint64_t collection_id = 0;
|
||||
std::uint64_t monotonic_ns = 0;
|
||||
std::vector<SweepTraceBlock> traces{};
|
||||
};
|
||||
|
||||
using PreprocessedCollection = RawSweepCollection;
|
||||
|
||||
enum class ResultKind : std::uint8_t {
|
||||
TraceComplex = 1,
|
||||
ScalarF32 = 2,
|
||||
};
|
||||
|
||||
struct ResultPayload {
|
||||
std::string processing_name{};
|
||||
ResultKind kind = ResultKind::TraceComplex;
|
||||
// Valid for TraceComplex payloads.
|
||||
std::vector<float> frequency_hz{};
|
||||
// Valid for TraceComplex payloads.
|
||||
std::vector<Complex32> trace{};
|
||||
// Valid for ScalarF32 payloads.
|
||||
float scalar_value = 0.0F;
|
||||
};
|
||||
|
||||
struct ResultBlock {
|
||||
ComboKey combo{};
|
||||
std::vector<ResultPayload> payloads{};
|
||||
};
|
||||
|
||||
struct ResultCollection {
|
||||
std::uint64_t collection_id = 0;
|
||||
std::uint64_t monotonic_ns = 0;
|
||||
std::vector<ResultBlock> blocks{};
|
||||
};
|
||||
|
||||
// Wire-format serialization helpers for IPC rings.
|
||||
[[nodiscard]] auto serialize_raw_collection(const RawSweepCollection& collection) -> std::vector<std::uint8_t>;
|
||||
[[nodiscard]] auto deserialize_raw_collection(std::span<const std::uint8_t> bytes) -> RawSweepCollection;
|
||||
|
||||
[[nodiscard]] auto serialize_preprocessed_collection(const PreprocessedCollection& collection)
|
||||
-> std::vector<std::uint8_t>;
|
||||
[[nodiscard]] auto deserialize_preprocessed_collection(std::span<const std::uint8_t> bytes)
|
||||
-> PreprocessedCollection;
|
||||
|
||||
[[nodiscard]] auto serialize_result_collection(const ResultCollection& collection) -> std::vector<std::uint8_t>;
|
||||
[[nodiscard]] auto deserialize_result_collection(std::span<const std::uint8_t> bytes) -> ResultCollection;
|
||||
|
||||
// Monotonic timestamp helper used for produced collections.
|
||||
[[nodiscard]] auto current_monotonic_ns() -> std::uint64_t;
|
||||
|
||||
} // namespace radar::ipc
|
||||
@@ -0,0 +1,61 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace radar::ipc {
|
||||
|
||||
class ShmRing {
|
||||
public:
|
||||
ShmRing() = default;
|
||||
~ShmRing();
|
||||
|
||||
ShmRing(const ShmRing&) = delete;
|
||||
auto operator=(const ShmRing&) -> ShmRing& = delete;
|
||||
|
||||
ShmRing(ShmRing&& other) noexcept;
|
||||
auto operator=(ShmRing&& other) noexcept -> ShmRing&;
|
||||
|
||||
[[nodiscard]] static auto open_or_create(
|
||||
const std::string& name,
|
||||
std::uint32_t capacity,
|
||||
std::uint32_t slot_size_bytes
|
||||
) -> ShmRing;
|
||||
|
||||
// Opens ring by name and validates wire-format header fields.
|
||||
[[nodiscard]] static auto open_existing(const std::string& name) -> ShmRing;
|
||||
// Removes shared memory object by name (safe if it does not exist).
|
||||
static void unlink_ring(const std::string& name);
|
||||
|
||||
[[nodiscard]] auto is_open() const -> bool;
|
||||
[[nodiscard]] auto capacity() const -> std::uint32_t;
|
||||
[[nodiscard]] auto slot_size_bytes() const -> std::uint32_t;
|
||||
[[nodiscard]] auto dropped_count() const -> std::uint64_t;
|
||||
|
||||
// Pushes payload with overwrite-oldest policy on overflow.
|
||||
[[nodiscard]] auto push(std::span<const std::uint8_t> payload) -> bool;
|
||||
// Pops the next payload if available.
|
||||
[[nodiscard]] auto pop(std::vector<std::uint8_t>& payload) -> bool;
|
||||
|
||||
private:
|
||||
struct Header;
|
||||
struct SlotHeader;
|
||||
|
||||
int fd_ = -1;
|
||||
std::size_t mapped_size_ = 0;
|
||||
void* mapped_ = nullptr;
|
||||
Header* header_ = nullptr;
|
||||
|
||||
// Slot geometry helpers.
|
||||
[[nodiscard]] auto slot_stride_bytes() const -> std::size_t;
|
||||
[[nodiscard]] auto slot_header(std::uint64_t sequence) const -> SlotHeader*;
|
||||
[[nodiscard]] auto slot_payload(SlotHeader* slot) const -> std::uint8_t*;
|
||||
|
||||
void close();
|
||||
static void validate_name(const std::string& name);
|
||||
};
|
||||
|
||||
} // namespace radar::ipc
|
||||
@@ -0,0 +1,336 @@
|
||||
#include "shared_types.hpp"
|
||||
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace radar::ipc {
|
||||
namespace {
|
||||
|
||||
constexpr std::uint32_t kRawCollectionMagic = 0x31574152U; // RAW1
|
||||
constexpr std::uint32_t kPreprocessedCollectionMagic = 0x31525050U; // PRP1
|
||||
constexpr std::uint32_t kResultCollectionMagic = 0x314C5352U; // RSL1
|
||||
|
||||
template <typename T>
|
||||
concept TriviallySerializable = std::is_trivially_copyable_v<T>;
|
||||
|
||||
[[nodiscard]] auto checked_count_to_u32(std::size_t count, const std::string& label) -> std::uint32_t {
|
||||
if (count > std::numeric_limits<std::uint32_t>::max()) {
|
||||
throw std::runtime_error(label + " exceeds uint32 wire-format limit");
|
||||
}
|
||||
return static_cast<std::uint32_t>(count);
|
||||
}
|
||||
|
||||
void ensure_equal_sizes(std::size_t left, std::size_t right, const std::string& label) {
|
||||
if (left != right) {
|
||||
throw std::runtime_error(label + " has inconsistent vector sizes");
|
||||
}
|
||||
}
|
||||
|
||||
class BinaryWriter {
|
||||
public:
|
||||
template <TriviallySerializable T>
|
||||
void write(const T& value) {
|
||||
const auto old_size = bytes_.size();
|
||||
bytes_.resize(old_size + sizeof(T));
|
||||
std::memcpy(bytes_.data() + old_size, &value, sizeof(T));
|
||||
}
|
||||
|
||||
void write_string(const std::string& value) {
|
||||
if (value.size() > std::numeric_limits<std::uint16_t>::max()) {
|
||||
throw std::runtime_error("String is too large for wire format");
|
||||
}
|
||||
|
||||
write(static_cast<std::uint16_t>(value.size()));
|
||||
|
||||
const auto old_size = bytes_.size();
|
||||
bytes_.resize(old_size + value.size());
|
||||
std::memcpy(bytes_.data() + old_size, value.data(), value.size());
|
||||
}
|
||||
|
||||
[[nodiscard]] auto finish() && -> std::vector<std::uint8_t> {
|
||||
return std::move(bytes_);
|
||||
}
|
||||
|
||||
private:
|
||||
std::vector<std::uint8_t> bytes_{};
|
||||
};
|
||||
|
||||
class BinaryReader {
|
||||
public:
|
||||
explicit BinaryReader(std::span<const std::uint8_t> bytes) : bytes_(bytes) {}
|
||||
|
||||
template <TriviallySerializable T>
|
||||
[[nodiscard]] auto read() -> T {
|
||||
ensure_available(sizeof(T));
|
||||
|
||||
T value{};
|
||||
std::memcpy(&value, bytes_.data() + offset_, sizeof(T));
|
||||
offset_ += sizeof(T);
|
||||
return value;
|
||||
}
|
||||
|
||||
[[nodiscard]] auto read_string() -> std::string {
|
||||
const auto size = read<std::uint16_t>();
|
||||
ensure_available(size);
|
||||
|
||||
const auto* begin = reinterpret_cast<const char*>(bytes_.data() + offset_);
|
||||
std::string value(begin, begin + size);
|
||||
offset_ += size;
|
||||
return value;
|
||||
}
|
||||
|
||||
[[nodiscard]] auto is_consumed() const -> bool {
|
||||
return offset_ == bytes_.size();
|
||||
}
|
||||
|
||||
private:
|
||||
void ensure_available(std::size_t size) const {
|
||||
if (offset_ + size > bytes_.size()) {
|
||||
throw std::runtime_error("Unexpected end of serialized payload");
|
||||
}
|
||||
}
|
||||
|
||||
std::span<const std::uint8_t> bytes_{};
|
||||
std::size_t offset_ = 0;
|
||||
};
|
||||
|
||||
void write_trace_block(BinaryWriter& writer, const SweepTraceBlock& trace) {
|
||||
ensure_equal_sizes(trace.frequency_hz.size(), trace.s21.size(), "Sweep trace");
|
||||
|
||||
writer.write(trace.combo.input_pos);
|
||||
writer.write(trace.combo.output_pos);
|
||||
writer.write(checked_count_to_u32(trace.frequency_hz.size(), "Trace point count"));
|
||||
|
||||
for (const auto frequency_hz : trace.frequency_hz) {
|
||||
writer.write(frequency_hz);
|
||||
}
|
||||
|
||||
for (const auto& point : trace.s21) {
|
||||
writer.write(point.re);
|
||||
writer.write(point.im);
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] auto read_trace_block(BinaryReader& reader) -> SweepTraceBlock {
|
||||
SweepTraceBlock trace{};
|
||||
trace.combo.input_pos = reader.read<std::uint32_t>();
|
||||
trace.combo.output_pos = reader.read<std::uint32_t>();
|
||||
|
||||
const auto point_count = reader.read<std::uint32_t>();
|
||||
trace.frequency_hz.reserve(point_count);
|
||||
trace.s21.reserve(point_count);
|
||||
|
||||
for (std::uint32_t index = 0; index < point_count; ++index) {
|
||||
trace.frequency_hz.push_back(reader.read<float>());
|
||||
}
|
||||
|
||||
for (std::uint32_t index = 0; index < point_count; ++index) {
|
||||
trace.s21.push_back(Complex32{
|
||||
.re = reader.read<float>(),
|
||||
.im = reader.read<float>(),
|
||||
});
|
||||
}
|
||||
|
||||
return trace;
|
||||
}
|
||||
|
||||
void write_trace_collection(BinaryWriter& writer, std::uint32_t magic, const RawSweepCollection& collection) {
|
||||
writer.write(magic);
|
||||
writer.write(collection.collection_id);
|
||||
writer.write(collection.monotonic_ns);
|
||||
writer.write(checked_count_to_u32(collection.traces.size(), "Trace count"));
|
||||
|
||||
for (const auto& trace : collection.traces) {
|
||||
write_trace_block(writer, trace);
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] auto read_trace_collection(BinaryReader& reader, std::uint32_t expected_magic) -> RawSweepCollection {
|
||||
const auto magic = reader.read<std::uint32_t>();
|
||||
if (magic != expected_magic) {
|
||||
throw std::runtime_error("Unexpected trace collection magic");
|
||||
}
|
||||
|
||||
RawSweepCollection collection{};
|
||||
collection.collection_id = reader.read<std::uint64_t>();
|
||||
collection.monotonic_ns = reader.read<std::uint64_t>();
|
||||
|
||||
const auto trace_count = reader.read<std::uint32_t>();
|
||||
collection.traces.reserve(trace_count);
|
||||
for (std::uint32_t index = 0; index < trace_count; ++index) {
|
||||
collection.traces.push_back(read_trace_block(reader));
|
||||
}
|
||||
|
||||
return collection;
|
||||
}
|
||||
|
||||
void write_trace_result_payload(BinaryWriter& writer, const ResultPayload& payload) {
|
||||
ensure_equal_sizes(payload.frequency_hz.size(), payload.trace.size(), "Result trace payload");
|
||||
|
||||
writer.write(checked_count_to_u32(payload.trace.size(), "Result trace point count"));
|
||||
for (const auto frequency_hz : payload.frequency_hz) {
|
||||
writer.write(frequency_hz);
|
||||
}
|
||||
for (const auto& sample : payload.trace) {
|
||||
writer.write(sample.re);
|
||||
writer.write(sample.im);
|
||||
}
|
||||
}
|
||||
|
||||
auto read_trace_result_payload(BinaryReader& reader, ResultPayload* payload) -> void {
|
||||
const auto point_count = reader.read<std::uint32_t>();
|
||||
|
||||
payload->frequency_hz.reserve(point_count);
|
||||
payload->trace.reserve(point_count);
|
||||
|
||||
for (std::uint32_t index = 0; index < point_count; ++index) {
|
||||
payload->frequency_hz.push_back(reader.read<float>());
|
||||
}
|
||||
for (std::uint32_t index = 0; index < point_count; ++index) {
|
||||
payload->trace.push_back(Complex32{
|
||||
.re = reader.read<float>(),
|
||||
.im = reader.read<float>(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void write_result_payload(BinaryWriter& writer, const ResultPayload& payload) {
|
||||
writer.write(static_cast<std::uint8_t>(payload.kind));
|
||||
writer.write_string(payload.processing_name);
|
||||
|
||||
switch (payload.kind) {
|
||||
case ResultKind::TraceComplex:
|
||||
write_trace_result_payload(writer, payload);
|
||||
return;
|
||||
case ResultKind::ScalarF32:
|
||||
writer.write(payload.scalar_value);
|
||||
return;
|
||||
default:
|
||||
throw std::runtime_error("Unsupported result payload kind");
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] auto read_result_payload(BinaryReader& reader) -> ResultPayload {
|
||||
ResultPayload payload{};
|
||||
payload.kind = static_cast<ResultKind>(reader.read<std::uint8_t>());
|
||||
payload.processing_name = reader.read_string();
|
||||
|
||||
switch (payload.kind) {
|
||||
case ResultKind::TraceComplex:
|
||||
read_trace_result_payload(reader, &payload);
|
||||
return payload;
|
||||
case ResultKind::ScalarF32:
|
||||
payload.scalar_value = reader.read<float>();
|
||||
return payload;
|
||||
default:
|
||||
throw std::runtime_error("Unsupported result payload kind in stream");
|
||||
}
|
||||
}
|
||||
|
||||
void write_result_block(BinaryWriter& writer, const ResultBlock& block) {
|
||||
writer.write(block.combo.input_pos);
|
||||
writer.write(block.combo.output_pos);
|
||||
writer.write(checked_count_to_u32(block.payloads.size(), "Result payload count"));
|
||||
|
||||
for (const auto& payload : block.payloads) {
|
||||
write_result_payload(writer, payload);
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] auto read_result_block(BinaryReader& reader) -> ResultBlock {
|
||||
ResultBlock block{};
|
||||
block.combo.input_pos = reader.read<std::uint32_t>();
|
||||
block.combo.output_pos = reader.read<std::uint32_t>();
|
||||
|
||||
const auto payload_count = reader.read<std::uint32_t>();
|
||||
block.payloads.reserve(payload_count);
|
||||
for (std::uint32_t index = 0; index < payload_count; ++index) {
|
||||
block.payloads.push_back(read_result_payload(reader));
|
||||
}
|
||||
|
||||
return block;
|
||||
}
|
||||
|
||||
void ensure_reader_consumed(const BinaryReader& reader, const std::string& payload_label) {
|
||||
if (!reader.is_consumed()) {
|
||||
throw std::runtime_error("Unexpected trailing bytes in " + payload_label);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
auto serialize_raw_collection(const RawSweepCollection& collection) -> std::vector<std::uint8_t> {
|
||||
BinaryWriter writer{};
|
||||
write_trace_collection(writer, kRawCollectionMagic, collection);
|
||||
return std::move(writer).finish();
|
||||
}
|
||||
|
||||
auto deserialize_raw_collection(std::span<const std::uint8_t> bytes) -> RawSweepCollection {
|
||||
BinaryReader reader(bytes);
|
||||
auto collection = read_trace_collection(reader, kRawCollectionMagic);
|
||||
ensure_reader_consumed(reader, "raw collection");
|
||||
return collection;
|
||||
}
|
||||
|
||||
auto serialize_preprocessed_collection(const PreprocessedCollection& collection) -> std::vector<std::uint8_t> {
|
||||
BinaryWriter writer{};
|
||||
write_trace_collection(writer, kPreprocessedCollectionMagic, collection);
|
||||
return std::move(writer).finish();
|
||||
}
|
||||
|
||||
auto deserialize_preprocessed_collection(std::span<const std::uint8_t> bytes) -> PreprocessedCollection {
|
||||
BinaryReader reader(bytes);
|
||||
auto collection = read_trace_collection(reader, kPreprocessedCollectionMagic);
|
||||
ensure_reader_consumed(reader, "preprocessed collection");
|
||||
return collection;
|
||||
}
|
||||
|
||||
auto serialize_result_collection(const ResultCollection& collection) -> std::vector<std::uint8_t> {
|
||||
BinaryWriter writer{};
|
||||
writer.write(kResultCollectionMagic);
|
||||
writer.write(collection.collection_id);
|
||||
writer.write(collection.monotonic_ns);
|
||||
writer.write(checked_count_to_u32(collection.blocks.size(), "Result block count"));
|
||||
|
||||
for (const auto& block : collection.blocks) {
|
||||
write_result_block(writer, block);
|
||||
}
|
||||
|
||||
return std::move(writer).finish();
|
||||
}
|
||||
|
||||
auto deserialize_result_collection(std::span<const std::uint8_t> bytes) -> ResultCollection {
|
||||
BinaryReader reader(bytes);
|
||||
|
||||
const auto magic = reader.read<std::uint32_t>();
|
||||
if (magic != kResultCollectionMagic) {
|
||||
throw std::runtime_error("Unexpected result collection magic");
|
||||
}
|
||||
|
||||
ResultCollection collection{};
|
||||
collection.collection_id = reader.read<std::uint64_t>();
|
||||
collection.monotonic_ns = reader.read<std::uint64_t>();
|
||||
|
||||
const auto block_count = reader.read<std::uint32_t>();
|
||||
collection.blocks.reserve(block_count);
|
||||
for (std::uint32_t index = 0; index < block_count; ++index) {
|
||||
collection.blocks.push_back(read_result_block(reader));
|
||||
}
|
||||
|
||||
ensure_reader_consumed(reader, "result collection");
|
||||
return collection;
|
||||
}
|
||||
|
||||
auto current_monotonic_ns() -> std::uint64_t {
|
||||
const auto now = std::chrono::steady_clock::now().time_since_epoch();
|
||||
const auto now_ns = std::chrono::duration_cast<std::chrono::nanoseconds>(now).count();
|
||||
return static_cast<std::uint64_t>(now_ns);
|
||||
}
|
||||
|
||||
} // namespace radar::ipc
|
||||
@@ -0,0 +1,380 @@
|
||||
#include "shm_ring.hpp"
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cerrno>
|
||||
#include <cstring>
|
||||
#include <fcntl.h>
|
||||
#include <new>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <sys/mman.h>
|
||||
#include <sys/stat.h>
|
||||
#include <thread>
|
||||
#include <unistd.h>
|
||||
#include <utility>
|
||||
|
||||
namespace radar::ipc {
|
||||
|
||||
struct alignas(64) ShmRing::Header {
|
||||
char magic[8]{};
|
||||
std::uint32_t version = 0;
|
||||
std::uint32_t capacity = 0;
|
||||
std::uint32_t slot_size_bytes = 0;
|
||||
std::uint32_t reserved = 0;
|
||||
std::atomic<std::uint64_t> write_seq{};
|
||||
std::atomic<std::uint64_t> read_seq{};
|
||||
std::atomic<std::uint64_t> dropped{};
|
||||
};
|
||||
|
||||
struct alignas(16) ShmRing::SlotHeader {
|
||||
std::uint32_t payload_size = 0;
|
||||
std::uint32_t reserved = 0;
|
||||
std::uint64_t sequence = 0;
|
||||
};
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr std::string_view kRingMagic = "RDRRING2";
|
||||
constexpr std::uint32_t kRingVersion = 1;
|
||||
constexpr auto kHeaderInitWaitTimeout = std::chrono::milliseconds(1000);
|
||||
constexpr auto kHeaderInitPollInterval = std::chrono::milliseconds(2);
|
||||
|
||||
[[nodiscard]] auto checked_u64_diff(std::uint64_t left, std::uint64_t right) -> std::uint64_t {
|
||||
return left >= right ? left - right : 0;
|
||||
}
|
||||
|
||||
[[nodiscard]] auto errno_message(const std::string& action, const std::string& name) -> std::runtime_error {
|
||||
return std::runtime_error(action + " " + name + ": " + std::strerror(errno));
|
||||
}
|
||||
|
||||
class ScopedFd {
|
||||
public:
|
||||
explicit ScopedFd(int fd) : fd_(fd) {}
|
||||
|
||||
~ScopedFd() {
|
||||
if (fd_ >= 0) {
|
||||
::close(fd_);
|
||||
}
|
||||
}
|
||||
|
||||
ScopedFd(const ScopedFd&) = delete;
|
||||
auto operator=(const ScopedFd&) -> ScopedFd& = delete;
|
||||
|
||||
ScopedFd(ScopedFd&& other) noexcept : fd_(std::exchange(other.fd_, -1)) {}
|
||||
|
||||
auto release() -> int {
|
||||
return std::exchange(fd_, -1);
|
||||
}
|
||||
|
||||
private:
|
||||
int fd_ = -1;
|
||||
};
|
||||
|
||||
class ScopedMmap {
|
||||
public:
|
||||
ScopedMmap(void* address, std::size_t size) : address_(address), size_(size) {}
|
||||
|
||||
~ScopedMmap() {
|
||||
if (address_ != nullptr && address_ != MAP_FAILED) {
|
||||
munmap(address_, size_);
|
||||
}
|
||||
}
|
||||
|
||||
ScopedMmap(const ScopedMmap&) = delete;
|
||||
auto operator=(const ScopedMmap&) -> ScopedMmap& = delete;
|
||||
|
||||
ScopedMmap(ScopedMmap&& other) noexcept
|
||||
: address_(std::exchange(other.address_, nullptr)),
|
||||
size_(std::exchange(other.size_, 0U)) {}
|
||||
|
||||
auto release() -> std::pair<void*, std::size_t> {
|
||||
return {std::exchange(address_, nullptr), std::exchange(size_, 0U)};
|
||||
}
|
||||
|
||||
private:
|
||||
void* address_ = nullptr;
|
||||
std::size_t size_ = 0;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
ShmRing::~ShmRing() {
|
||||
close();
|
||||
}
|
||||
|
||||
ShmRing::ShmRing(ShmRing&& other) noexcept {
|
||||
*this = std::move(other);
|
||||
}
|
||||
|
||||
auto ShmRing::operator=(ShmRing&& other) noexcept -> ShmRing& {
|
||||
if (this != &other) {
|
||||
close();
|
||||
|
||||
fd_ = std::exchange(other.fd_, -1);
|
||||
mapped_size_ = std::exchange(other.mapped_size_, 0U);
|
||||
mapped_ = std::exchange(other.mapped_, nullptr);
|
||||
header_ = std::exchange(other.header_, nullptr);
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
auto ShmRing::open_or_create(
|
||||
const std::string& name,
|
||||
std::uint32_t capacity,
|
||||
std::uint32_t slot_size_bytes
|
||||
) -> ShmRing {
|
||||
validate_name(name);
|
||||
if (capacity == 0U) {
|
||||
throw std::runtime_error("Ring capacity must be > 0");
|
||||
}
|
||||
if (slot_size_bytes == 0U) {
|
||||
throw std::runtime_error("Ring slot size must be > 0");
|
||||
}
|
||||
|
||||
bool created = false;
|
||||
int fd = shm_open(name.c_str(), O_RDWR | O_CREAT | O_EXCL, 0660);
|
||||
if (fd >= 0) {
|
||||
created = true;
|
||||
} else if (errno == EEXIST) {
|
||||
fd = shm_open(name.c_str(), O_RDWR, 0660);
|
||||
}
|
||||
|
||||
if (fd < 0) {
|
||||
throw errno_message("Failed to open shared memory ring", name);
|
||||
}
|
||||
|
||||
ScopedFd scoped_fd(fd);
|
||||
|
||||
const auto slot_stride = sizeof(SlotHeader) + static_cast<std::size_t>(slot_size_bytes);
|
||||
const auto mapped_size = sizeof(Header) + slot_stride * capacity;
|
||||
|
||||
if (created) {
|
||||
if (ftruncate(fd, static_cast<off_t>(mapped_size)) != 0) {
|
||||
throw errno_message("Failed to resize shared memory ring", name);
|
||||
}
|
||||
}
|
||||
|
||||
void* mapped = mmap(nullptr, mapped_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
|
||||
if (mapped == MAP_FAILED) {
|
||||
throw errno_message("Failed to mmap shared memory ring", name);
|
||||
}
|
||||
|
||||
ScopedMmap scoped_mmap(mapped, mapped_size);
|
||||
auto* header = static_cast<Header*>(mapped);
|
||||
|
||||
if (created) {
|
||||
std::memset(mapped, 0, mapped_size);
|
||||
std::memcpy(header->magic, kRingMagic.data(), kRingMagic.size());
|
||||
header->version = kRingVersion;
|
||||
header->capacity = capacity;
|
||||
header->slot_size_bytes = slot_size_bytes;
|
||||
|
||||
new (&header->write_seq) std::atomic<std::uint64_t>(0);
|
||||
new (&header->read_seq) std::atomic<std::uint64_t>(0);
|
||||
new (&header->dropped) std::atomic<std::uint64_t>(0);
|
||||
std::atomic_thread_fence(std::memory_order_release);
|
||||
} else {
|
||||
const auto deadline = std::chrono::steady_clock::now() + kHeaderInitWaitTimeout;
|
||||
|
||||
while (true) {
|
||||
std::atomic_thread_fence(std::memory_order_acquire);
|
||||
|
||||
const bool magic_ok = std::memcmp(header->magic, kRingMagic.data(), kRingMagic.size()) == 0;
|
||||
const bool version_ok = header->version == kRingVersion;
|
||||
const bool geometry_ok = header->capacity == capacity && header->slot_size_bytes == slot_size_bytes;
|
||||
|
||||
if (magic_ok && version_ok && geometry_ok) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (std::chrono::steady_clock::now() >= deadline) {
|
||||
if (!magic_ok) {
|
||||
throw std::runtime_error("Shared memory ring magic mismatch for " + name);
|
||||
}
|
||||
if (!version_ok) {
|
||||
throw std::runtime_error("Shared memory ring version mismatch for " + name);
|
||||
}
|
||||
throw std::runtime_error("Shared memory ring geometry mismatch for " + name);
|
||||
}
|
||||
|
||||
std::this_thread::sleep_for(kHeaderInitPollInterval);
|
||||
}
|
||||
}
|
||||
|
||||
ShmRing ring{};
|
||||
ring.fd_ = scoped_fd.release();
|
||||
|
||||
const auto [released_mapped, released_size] = scoped_mmap.release();
|
||||
ring.mapped_ = released_mapped;
|
||||
ring.mapped_size_ = released_size;
|
||||
ring.header_ = static_cast<Header*>(released_mapped);
|
||||
return ring;
|
||||
}
|
||||
|
||||
auto ShmRing::open_existing(const std::string& name) -> ShmRing {
|
||||
validate_name(name);
|
||||
|
||||
const int fd = shm_open(name.c_str(), O_RDWR, 0660);
|
||||
if (fd < 0) {
|
||||
throw errno_message("Failed to open shared memory ring", name);
|
||||
}
|
||||
|
||||
ScopedFd scoped_fd(fd);
|
||||
|
||||
struct stat info {};
|
||||
if (fstat(fd, &info) != 0) {
|
||||
throw errno_message("Failed to stat shared memory ring", name);
|
||||
}
|
||||
|
||||
if (info.st_size < static_cast<off_t>(sizeof(Header))) {
|
||||
throw std::runtime_error("Shared memory ring size is too small for " + name);
|
||||
}
|
||||
|
||||
const auto mapped_size = static_cast<std::size_t>(info.st_size);
|
||||
void* mapped = mmap(nullptr, mapped_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
|
||||
if (mapped == MAP_FAILED) {
|
||||
throw errno_message("Failed to mmap shared memory ring", name);
|
||||
}
|
||||
|
||||
ScopedMmap scoped_mmap(mapped, mapped_size);
|
||||
auto* header = static_cast<Header*>(mapped);
|
||||
|
||||
if (std::memcmp(header->magic, kRingMagic.data(), kRingMagic.size()) != 0) {
|
||||
throw std::runtime_error("Shared memory ring magic mismatch for " + name);
|
||||
}
|
||||
if (header->version != kRingVersion) {
|
||||
throw std::runtime_error("Shared memory ring version mismatch for " + name);
|
||||
}
|
||||
|
||||
ShmRing ring{};
|
||||
ring.fd_ = scoped_fd.release();
|
||||
|
||||
const auto [released_mapped, released_size] = scoped_mmap.release();
|
||||
ring.mapped_ = released_mapped;
|
||||
ring.mapped_size_ = released_size;
|
||||
ring.header_ = static_cast<Header*>(released_mapped);
|
||||
return ring;
|
||||
}
|
||||
|
||||
void ShmRing::unlink_ring(const std::string& name) {
|
||||
validate_name(name);
|
||||
if (shm_unlink(name.c_str()) != 0 && errno != ENOENT) {
|
||||
throw errno_message("Failed to unlink shared memory ring", name);
|
||||
}
|
||||
}
|
||||
|
||||
auto ShmRing::is_open() const -> bool {
|
||||
return header_ != nullptr;
|
||||
}
|
||||
|
||||
auto ShmRing::capacity() const -> std::uint32_t {
|
||||
return header_ != nullptr ? header_->capacity : 0;
|
||||
}
|
||||
|
||||
auto ShmRing::slot_size_bytes() const -> std::uint32_t {
|
||||
return header_ != nullptr ? header_->slot_size_bytes : 0;
|
||||
}
|
||||
|
||||
auto ShmRing::dropped_count() const -> std::uint64_t {
|
||||
if (header_ == nullptr) {
|
||||
return 0;
|
||||
}
|
||||
return header_->dropped.load(std::memory_order_acquire);
|
||||
}
|
||||
|
||||
auto ShmRing::push(std::span<const std::uint8_t> payload) -> bool {
|
||||
if (header_ == nullptr) {
|
||||
throw std::runtime_error("Ring is not open");
|
||||
}
|
||||
if (payload.size() > header_->slot_size_bytes) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto write_seq = header_->write_seq.load(std::memory_order_relaxed);
|
||||
const auto read_seq = header_->read_seq.load(std::memory_order_acquire);
|
||||
|
||||
if (checked_u64_diff(write_seq, read_seq) >= header_->capacity) {
|
||||
header_->read_seq.store(read_seq + 1U, std::memory_order_release);
|
||||
header_->dropped.fetch_add(1U, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
auto* slot = slot_header(write_seq);
|
||||
slot->payload_size = static_cast<std::uint32_t>(payload.size());
|
||||
slot->sequence = write_seq + 1U;
|
||||
std::memcpy(slot_payload(slot), payload.data(), payload.size());
|
||||
|
||||
std::atomic_thread_fence(std::memory_order_release);
|
||||
header_->write_seq.store(write_seq + 1U, std::memory_order_release);
|
||||
return true;
|
||||
}
|
||||
|
||||
auto ShmRing::pop(std::vector<std::uint8_t>& payload) -> bool {
|
||||
if (header_ == nullptr) {
|
||||
throw std::runtime_error("Ring is not open");
|
||||
}
|
||||
|
||||
const auto read_seq = header_->read_seq.load(std::memory_order_relaxed);
|
||||
const auto write_seq = header_->write_seq.load(std::memory_order_acquire);
|
||||
if (read_seq >= write_seq) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto* slot = slot_header(read_seq);
|
||||
if (slot->sequence != read_seq + 1U) {
|
||||
// Producer overwrote this slot before consumer read it. Resync to latest.
|
||||
header_->read_seq.store(write_seq, std::memory_order_release);
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto payload_size = slot->payload_size;
|
||||
if (payload_size > header_->slot_size_bytes) {
|
||||
header_->read_seq.store(write_seq, std::memory_order_release);
|
||||
throw std::runtime_error("Invalid payload size in shared memory slot");
|
||||
}
|
||||
|
||||
payload.resize(payload_size);
|
||||
std::memcpy(payload.data(), slot_payload(slot), payload_size);
|
||||
|
||||
std::atomic_thread_fence(std::memory_order_acquire);
|
||||
header_->read_seq.store(read_seq + 1U, std::memory_order_release);
|
||||
return true;
|
||||
}
|
||||
|
||||
auto ShmRing::slot_stride_bytes() const -> std::size_t {
|
||||
return sizeof(SlotHeader) + header_->slot_size_bytes;
|
||||
}
|
||||
|
||||
auto ShmRing::slot_header(std::uint64_t sequence) const -> SlotHeader* {
|
||||
const auto index = sequence % header_->capacity;
|
||||
auto* slots_begin = static_cast<std::uint8_t*>(mapped_) + sizeof(Header);
|
||||
return reinterpret_cast<SlotHeader*>(slots_begin + index * slot_stride_bytes());
|
||||
}
|
||||
|
||||
auto ShmRing::slot_payload(SlotHeader* slot) const -> std::uint8_t* {
|
||||
return reinterpret_cast<std::uint8_t*>(slot) + sizeof(SlotHeader);
|
||||
}
|
||||
|
||||
void ShmRing::close() {
|
||||
if (mapped_ != nullptr) {
|
||||
munmap(mapped_, mapped_size_);
|
||||
mapped_ = nullptr;
|
||||
}
|
||||
|
||||
if (fd_ >= 0) {
|
||||
::close(fd_);
|
||||
fd_ = -1;
|
||||
}
|
||||
|
||||
mapped_size_ = 0;
|
||||
header_ = nullptr;
|
||||
}
|
||||
|
||||
void ShmRing::validate_name(const std::string& name) {
|
||||
if (name.empty() || name.front() != '/') {
|
||||
throw std::runtime_error("POSIX shm name must start with '/'");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace radar::ipc
|
||||
Reference in New Issue
Block a user