added s11 collections
This commit is contained in:
@@ -32,8 +32,10 @@ struct ComboKeyHash {
|
||||
|
||||
struct SweepTraceBlock {
|
||||
ComboKey combo{};
|
||||
// Frequency axis in Hz. Must have the same size as `s21`.
|
||||
// Frequency axis in Hz. Must have the same size as `s11` and `s21`.
|
||||
std::vector<float> frequency_hz{};
|
||||
// Complex S11 samples for matching frequency points.
|
||||
std::vector<Complex32> s11{};
|
||||
// Complex S21 samples for matching frequency points.
|
||||
std::vector<Complex32> s21{};
|
||||
};
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
namespace radar::ipc {
|
||||
namespace {
|
||||
|
||||
constexpr std::uint32_t kRawCollectionMagic = 0x31574152U; // RAW1
|
||||
constexpr std::uint32_t kPreprocessedCollectionMagic = 0x31525050U; // PRP1
|
||||
constexpr std::uint32_t kRawCollectionMagic = 0x32574152U; // RAW2
|
||||
constexpr std::uint32_t kPreprocessedCollectionMagic = 0x32525050U; // PRP2
|
||||
constexpr std::uint32_t kResultCollectionMagic = 0x314C5352U; // RSL1
|
||||
|
||||
template <typename T>
|
||||
@@ -102,6 +102,7 @@ class BinaryReader {
|
||||
|
||||
void write_trace_block(BinaryWriter& writer, const SweepTraceBlock& trace) {
|
||||
ensure_equal_sizes(trace.frequency_hz.size(), trace.s21.size(), "Sweep trace");
|
||||
ensure_equal_sizes(trace.frequency_hz.size(), trace.s11.size(), "Sweep trace S11");
|
||||
|
||||
writer.write(trace.combo.input_pos);
|
||||
writer.write(trace.combo.output_pos);
|
||||
@@ -111,6 +112,11 @@ void write_trace_block(BinaryWriter& writer, const SweepTraceBlock& trace) {
|
||||
writer.write(frequency_hz);
|
||||
}
|
||||
|
||||
for (const auto& point : trace.s11) {
|
||||
writer.write(point.re);
|
||||
writer.write(point.im);
|
||||
}
|
||||
|
||||
for (const auto& point : trace.s21) {
|
||||
writer.write(point.re);
|
||||
writer.write(point.im);
|
||||
@@ -124,12 +130,20 @@ void write_trace_block(BinaryWriter& writer, const SweepTraceBlock& trace) {
|
||||
|
||||
const auto point_count = reader.read<std::uint32_t>();
|
||||
trace.frequency_hz.reserve(point_count);
|
||||
trace.s11.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.s11.push_back(Complex32{
|
||||
.re = reader.read<float>(),
|
||||
.im = reader.read<float>(),
|
||||
});
|
||||
}
|
||||
|
||||
for (std::uint32_t index = 0; index < point_count; ++index) {
|
||||
trace.s21.push_back(Complex32{
|
||||
.re = reader.read<float>(),
|
||||
|
||||
@@ -30,6 +30,11 @@ void validate_trace_layout(const ipc::SweepTraceBlock& trace, const std::string&
|
||||
trace_label + " frequency/complex vector size mismatch for combo " + combo_to_string(trace.combo)
|
||||
);
|
||||
}
|
||||
if (trace.frequency_hz.size() != trace.s11.size()) {
|
||||
throw std::runtime_error(
|
||||
trace_label + " frequency/S11 vector size mismatch for combo " + combo_to_string(trace.combo)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -93,6 +98,7 @@ auto CalibrationMaster::apply(const ipc::SweepTraceBlock& measured_trace) const
|
||||
ipc::SweepTraceBlock output{};
|
||||
output.combo = measured_trace.combo;
|
||||
output.frequency_hz = measured_trace.frequency_hz;
|
||||
output.s11 = measured_trace.s11;
|
||||
output.s21 = calibrator_impl_->apply(measured_trace.s21, standard.s21);
|
||||
|
||||
return output;
|
||||
|
||||
@@ -35,6 +35,11 @@ void validate_trace_layout(const ipc::SweepTraceBlock& trace, const std::string&
|
||||
trace_label + " frequency/complex vector size mismatch for combo " + combo_to_string(trace.combo)
|
||||
);
|
||||
}
|
||||
if (trace.frequency_hz.size() != trace.s11.size()) {
|
||||
throw std::runtime_error(
|
||||
trace_label + " frequency/S11 vector size mismatch for combo " + combo_to_string(trace.combo)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -115,6 +120,7 @@ auto ReferenceMaster::apply(const ipc::SweepTraceBlock& calibrated_trace) const
|
||||
ipc::SweepTraceBlock output{};
|
||||
output.combo = calibrated_trace.combo;
|
||||
output.frequency_hz = calibrated_trace.frequency_hz;
|
||||
output.s11 = calibrated_trace.s11;
|
||||
output.s21.resize(calibrated_trace.s21.size());
|
||||
|
||||
static_assert(sizeof(ipc::Complex32) == sizeof(float) * 2U, "Complex32 layout must be two contiguous floats");
|
||||
|
||||
+7
-5
@@ -7,12 +7,13 @@
|
||||
namespace radar::drivers {
|
||||
|
||||
/**
|
||||
* @brief One S21 sweep acquired from the radar.
|
||||
* @brief One forward sweep acquired from the radar.
|
||||
*
|
||||
* Both vectors must have equal size and aligned indices.
|
||||
* All vectors must have equal size and aligned indices.
|
||||
*/
|
||||
struct SweepTrace {
|
||||
std::vector<float> frequency_hz{};
|
||||
std::vector<ipc::Complex32> s11{};
|
||||
std::vector<ipc::Complex32> s21{};
|
||||
};
|
||||
|
||||
@@ -20,7 +21,8 @@ struct SweepTrace {
|
||||
* @brief Minimal radar interface used by the sweep orchestrator.
|
||||
*
|
||||
* Implementations are expected to be lightweight: configuration is handled by
|
||||
* the Python layer, while this interface only opens/closes and acquires S21.
|
||||
* the Python layer, while this interface only opens/closes and acquires one
|
||||
* forward sweep.
|
||||
*/
|
||||
class RadarDriver {
|
||||
public:
|
||||
@@ -30,8 +32,8 @@ class RadarDriver {
|
||||
virtual void open() = 0;
|
||||
/** @brief Release all allocated resources. */
|
||||
virtual void close() = 0;
|
||||
/** @brief Acquire one S21 sweep. */
|
||||
[[nodiscard]] virtual auto acquire_s21_sweep() -> SweepTrace = 0;
|
||||
/** @brief Acquire one sweep containing the forward traces exposed by the driver. */
|
||||
[[nodiscard]] virtual auto acquire_sweep() -> SweepTrace = 0;
|
||||
};
|
||||
|
||||
} // namespace radar::drivers
|
||||
|
||||
+19
-12
@@ -68,7 +68,7 @@ void LibreVnaMinimalDriver::close() {
|
||||
is_open_ = false;
|
||||
}
|
||||
|
||||
auto LibreVnaMinimalDriver::acquire_s21_sweep() -> SweepTrace {
|
||||
auto LibreVnaMinimalDriver::acquire_sweep() -> SweepTrace {
|
||||
if (!is_open_) {
|
||||
throw std::runtime_error("Radar driver is not open");
|
||||
}
|
||||
@@ -111,6 +111,7 @@ auto LibreVnaMinimalDriver::acquire_s21_sweep() -> SweepTrace {
|
||||
auto LibreVnaMinimalDriver::acquire_mock() -> SweepTrace {
|
||||
SweepTrace trace{};
|
||||
trace.frequency_hz.reserve(settings_.sweep.points);
|
||||
trace.s11.reserve(settings_.sweep.points);
|
||||
trace.s21.reserve(settings_.sweep.points);
|
||||
|
||||
const auto span_hz = settings_.sweep.stop_hz - settings_.sweep.start_hz;
|
||||
@@ -127,7 +128,14 @@ auto LibreVnaMinimalDriver::acquire_mock() -> SweepTrace {
|
||||
sample.re = envelope * std::cos(phase);
|
||||
sample.im = envelope * std::sin(phase);
|
||||
|
||||
const auto reflection_phase = 0.7F * phase + 0.35F;
|
||||
const auto reflection_envelope = 0.15F + 0.1F * std::cos(0.25F * phase);
|
||||
ipc::Complex32 reflection{};
|
||||
reflection.re = reflection_envelope * std::cos(reflection_phase);
|
||||
reflection.im = reflection_envelope * std::sin(reflection_phase);
|
||||
|
||||
trace.frequency_hz.push_back(frequency_hz);
|
||||
trace.s11.push_back(reflection);
|
||||
trace.s21.push_back(sample);
|
||||
}
|
||||
|
||||
@@ -147,6 +155,7 @@ auto LibreVnaMinimalDriver::acquire_native() -> SweepTrace {
|
||||
|
||||
SweepTrace trace{};
|
||||
trace.frequency_hz.assign(settings_.sweep.points, 0.0F);
|
||||
trace.s11.assign(settings_.sweep.points, ipc::Complex32{});
|
||||
trace.s21.assign(settings_.sweep.points, ipc::Complex32{});
|
||||
|
||||
std::vector<std::uint8_t> received(settings_.sweep.points, 0U);
|
||||
@@ -165,24 +174,22 @@ auto LibreVnaMinimalDriver::acquire_native() -> SweepTrace {
|
||||
);
|
||||
}
|
||||
|
||||
std::uint32_t point_number = 0;
|
||||
float frequency_hz = 0.0F;
|
||||
ipc::Complex32 s21{};
|
||||
|
||||
if (!decode_vna_datapoint_s21(packet.payload, point_number, frequency_hz, s21)) {
|
||||
throw std::runtime_error("Failed to decode S21 from VNADatapoint packet");
|
||||
DecodedVnaDatapoint datapoint{};
|
||||
if (!decode_vna_datapoint_traces(packet.payload, datapoint)) {
|
||||
throw std::runtime_error("Failed to decode traces from VNADatapoint packet");
|
||||
}
|
||||
if (point_number >= settings_.sweep.points) {
|
||||
if (datapoint.point_number >= settings_.sweep.points) {
|
||||
throw std::runtime_error("Received out-of-range VNADatapoint index");
|
||||
}
|
||||
|
||||
if (received[point_number] == 0U) {
|
||||
received[point_number] = 1U;
|
||||
if (received[datapoint.point_number] == 0U) {
|
||||
received[datapoint.point_number] = 1U;
|
||||
++received_count;
|
||||
}
|
||||
|
||||
trace.frequency_hz[point_number] = frequency_hz;
|
||||
trace.s21[point_number] = s21;
|
||||
trace.frequency_hz[datapoint.point_number] = datapoint.frequency_hz;
|
||||
trace.s11[datapoint.point_number] = datapoint.s11;
|
||||
trace.s21[datapoint.point_number] = datapoint.s21;
|
||||
}
|
||||
|
||||
return trace;
|
||||
|
||||
+36
-18
@@ -35,11 +35,9 @@ auto LibreVnaMinimalDriver::encode_frame(
|
||||
return frame;
|
||||
}
|
||||
|
||||
auto LibreVnaMinimalDriver::decode_vna_datapoint_s21(
|
||||
auto LibreVnaMinimalDriver::decode_vna_datapoint_traces(
|
||||
std::span<const std::uint8_t> payload,
|
||||
std::uint32_t& point_number_out,
|
||||
float& frequency_out,
|
||||
ipc::Complex32& s21_out
|
||||
DecodedVnaDatapoint& datapoint_out
|
||||
) -> bool {
|
||||
// VNADatapoint payload layout:
|
||||
// [0..7]=freq_or_time, [8..9]=cdbm, [10..11]=point_number,
|
||||
@@ -58,17 +56,21 @@ auto LibreVnaMinimalDriver::decode_vna_datapoint_s21(
|
||||
return false;
|
||||
}
|
||||
|
||||
point_number_out = detail::read_u16_le(payload, 10);
|
||||
frequency_out = static_cast<float>(detail::read_u64_le(payload, 0));
|
||||
datapoint_out = DecodedVnaDatapoint{};
|
||||
datapoint_out.point_number = detail::read_u16_le(payload, 10);
|
||||
datapoint_out.frequency_hz = static_cast<float>(detail::read_u64_le(payload, 0));
|
||||
|
||||
const auto real_offset = 12U;
|
||||
const auto imag_offset = real_offset + (4U * num_values);
|
||||
const auto flags_offset = imag_offset + (4U * num_values);
|
||||
|
||||
std::array<std::complex<float>, 8> ref_by_stage{};
|
||||
std::array<std::complex<float>, 8> measured_by_stage{};
|
||||
std::array<std::complex<float>, 8> s11_measured_by_stage{};
|
||||
std::array<std::complex<float>, 8> s21_measured_by_stage{};
|
||||
std::array<bool, 8> has_ref{};
|
||||
std::array<bool, 8> has_measured{};
|
||||
std::array<bool, 8> has_s11_measured{};
|
||||
std::array<bool, 8> has_s21_measured{};
|
||||
constexpr float kReferenceMagnitudeSquaredEpsilon = 1e-12F;
|
||||
|
||||
for (std::size_t index = 0; index < num_values; ++index) {
|
||||
const auto flags = payload[flags_offset + index];
|
||||
@@ -87,25 +89,42 @@ auto LibreVnaMinimalDriver::decode_vna_datapoint_s21(
|
||||
ref_by_stage[stage] = value;
|
||||
has_ref[stage] = true;
|
||||
}
|
||||
if ((flags & detail::kPort1Mask) != 0U && !is_reference) {
|
||||
s11_measured_by_stage[stage] = value;
|
||||
has_s11_measured[stage] = true;
|
||||
}
|
||||
if ((flags & detail::kPort2Mask) != 0U && !is_reference) {
|
||||
measured_by_stage[stage] = value;
|
||||
has_measured[stage] = true;
|
||||
s21_measured_by_stage[stage] = value;
|
||||
has_s21_measured[stage] = true;
|
||||
}
|
||||
}
|
||||
|
||||
// We need one reference sample from port1 and one measured sample from port2.
|
||||
// For the forward sweep used in this project, port1 reference is the
|
||||
// incident signal, port1 measured is reflection (S11 numerator), and
|
||||
// port2 measured is transmission (S21 numerator).
|
||||
for (std::size_t stage = 0; stage < ref_by_stage.size(); ++stage) {
|
||||
if (!has_ref[stage] || !has_measured[stage]) {
|
||||
if (!has_ref[stage]) {
|
||||
continue;
|
||||
}
|
||||
if (std::norm(ref_by_stage[stage]) <= 0.0F) {
|
||||
if (std::norm(ref_by_stage[stage]) <= kReferenceMagnitudeSquaredEpsilon) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto ratio = measured_by_stage[stage] / ref_by_stage[stage];
|
||||
s21_out.re = ratio.real();
|
||||
s21_out.im = ratio.imag();
|
||||
return true;
|
||||
if (has_s11_measured[stage]) {
|
||||
const auto s11_ratio = s11_measured_by_stage[stage] / ref_by_stage[stage];
|
||||
datapoint_out.s11.re = s11_ratio.real();
|
||||
datapoint_out.s11.im = s11_ratio.imag();
|
||||
datapoint_out.has_s11 = true;
|
||||
}
|
||||
if (has_s21_measured[stage]) {
|
||||
const auto s21_ratio = s21_measured_by_stage[stage] / ref_by_stage[stage];
|
||||
datapoint_out.s21.re = s21_ratio.real();
|
||||
datapoint_out.s21.im = s21_ratio.imag();
|
||||
datapoint_out.has_s21 = true;
|
||||
}
|
||||
if (datapoint_out.has_s21) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
@@ -127,4 +146,3 @@ auto LibreVnaMinimalDriver::crc32(std::span<const std::uint8_t> data) -> std::ui
|
||||
}
|
||||
|
||||
} // namespace radar::drivers
|
||||
|
||||
|
||||
+16
-6
@@ -30,7 +30,7 @@ struct LibreVnaMinimalDriverSettings {
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Minimal radar driver that acquires S21 sweeps from LibreVNA.
|
||||
* @brief Minimal radar driver that acquires forward sweeps from LibreVNA.
|
||||
*
|
||||
* This class intentionally keeps scope narrow: open/close transport and
|
||||
* acquire one sweep. Full device configuration is expected to be done by the
|
||||
@@ -42,7 +42,7 @@ class LibreVnaMinimalDriver final : public RadarDriver {
|
||||
|
||||
void open() override;
|
||||
void close() override;
|
||||
[[nodiscard]] auto acquire_s21_sweep() -> SweepTrace override;
|
||||
[[nodiscard]] auto acquire_sweep() -> SweepTrace override;
|
||||
|
||||
private:
|
||||
/**
|
||||
@@ -53,6 +53,18 @@ class LibreVnaMinimalDriver final : public RadarDriver {
|
||||
std::vector<std::uint8_t> payload{};
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief One decoded VNADatapoint containing complex values for one point.
|
||||
*/
|
||||
struct DecodedVnaDatapoint {
|
||||
std::uint32_t point_number = 0;
|
||||
float frequency_hz = 0.0F;
|
||||
ipc::Complex32 s11{};
|
||||
ipc::Complex32 s21{};
|
||||
bool has_s11 = false;
|
||||
bool has_s21 = false;
|
||||
};
|
||||
|
||||
[[nodiscard]] auto acquire_mock() -> SweepTrace;
|
||||
[[nodiscard]] auto acquire_native() -> SweepTrace;
|
||||
|
||||
@@ -70,11 +82,9 @@ class LibreVnaMinimalDriver final : public RadarDriver {
|
||||
[[nodiscard]] auto pop_packet(std::uint8_t packet_type) -> std::optional<NativePacket>;
|
||||
[[nodiscard]] static auto encode_frame(std::uint8_t packet_type, std::span<const std::uint8_t> payload)
|
||||
-> std::vector<std::uint8_t>;
|
||||
[[nodiscard]] static auto decode_vna_datapoint_s21(
|
||||
[[nodiscard]] static auto decode_vna_datapoint_traces(
|
||||
std::span<const std::uint8_t> payload,
|
||||
std::uint32_t& point_number_out,
|
||||
float& frequency_out,
|
||||
ipc::Complex32& s21_out
|
||||
DecodedVnaDatapoint& datapoint_out
|
||||
) -> bool;
|
||||
[[nodiscard]] static auto crc32(std::span<const std::uint8_t> data) -> std::uint32_t;
|
||||
void validate_device_info_payload(std::span<const std::uint8_t> payload) const;
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
#include "radar_driver.hpp"
|
||||
#include "run_config.hpp"
|
||||
#include "shm_ring.hpp"
|
||||
#include "sweep_plan.hpp"
|
||||
#include "switch_driver.hpp"
|
||||
|
||||
namespace radar::acq {
|
||||
@@ -45,7 +44,7 @@ class SweepOrchestrator {
|
||||
|
||||
private:
|
||||
/**
|
||||
* @brief Acquire one collection for all combinations in `plan_`.
|
||||
* @brief Acquire one collection for all configured combinations.
|
||||
*/
|
||||
[[nodiscard]] auto acquire_one_collection(std::uint64_t collection_id, const std::atomic<bool>& stop_requested)
|
||||
-> ipc::RawSweepCollection;
|
||||
@@ -56,7 +55,6 @@ class SweepOrchestrator {
|
||||
drivers::SwitchDriver& output_switch_driver_;
|
||||
ipc::ShmRing& raw_ring_;
|
||||
ipc::ShmRing* raw_tap_ring_ = nullptr;
|
||||
SweepPlan plan_{};
|
||||
};
|
||||
|
||||
} // namespace radar::acq
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "run_config.hpp"
|
||||
#include "shared_types.hpp"
|
||||
|
||||
namespace radar::acq {
|
||||
|
||||
/**
|
||||
* @brief Pre-validated execution order of switch combinations for one collection.
|
||||
*/
|
||||
struct SweepPlan {
|
||||
std::vector<ipc::ComboKey> ordered_combos{};
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Build and validate sweep execution plan from runtime config.
|
||||
*/
|
||||
[[nodiscard]] auto build_sweep_plan(const config::RunConfig& config) -> SweepPlan;
|
||||
|
||||
} // namespace radar::acq
|
||||
@@ -21,7 +21,7 @@ void sleep_if_needed_ms(std::uint32_t delay_ms) {
|
||||
}
|
||||
|
||||
void validate_sweep(const drivers::SweepTrace& sweep) {
|
||||
if (sweep.frequency_hz.size() != sweep.s21.size()) {
|
||||
if (sweep.frequency_hz.size() != sweep.s11.size() || sweep.frequency_hz.size() != sweep.s21.size()) {
|
||||
throw std::runtime_error("Radar driver returned inconsistent sweep vectors");
|
||||
}
|
||||
}
|
||||
@@ -106,8 +106,7 @@ SweepOrchestrator::SweepOrchestrator(
|
||||
input_switch_driver_(input_switch_driver),
|
||||
output_switch_driver_(output_switch_driver),
|
||||
raw_ring_(raw_ring),
|
||||
raw_tap_ring_(raw_tap_ring),
|
||||
plan_(build_sweep_plan(config)) {}
|
||||
raw_tap_ring_(raw_tap_ring) {}
|
||||
|
||||
void SweepOrchestrator::run(const std::atomic<bool>& stop_requested) {
|
||||
DriverLifecycleGuard lifecycle_guard(radar_driver_, input_switch_driver_, output_switch_driver_);
|
||||
@@ -142,10 +141,10 @@ auto SweepOrchestrator::acquire_one_collection(
|
||||
ipc::RawSweepCollection collection{};
|
||||
collection.collection_id = collection_id;
|
||||
collection.monotonic_ns = ipc::current_monotonic_ns();
|
||||
collection.traces.reserve(plan_.ordered_combos.size());
|
||||
collection.traces.reserve(config_.run_combos.size());
|
||||
bool interrupted = false;
|
||||
|
||||
for (const auto& combo : plan_.ordered_combos) {
|
||||
for (const auto& combo : config_.run_combos) {
|
||||
if (should_stop(stop_requested)) {
|
||||
interrupted = true;
|
||||
break;
|
||||
@@ -157,12 +156,13 @@ auto SweepOrchestrator::acquire_one_collection(
|
||||
input_switch_driver_.switch_to(combo.input_pos);
|
||||
sleep_if_needed_ms(config_.runtime.settling_ms);
|
||||
|
||||
auto sweep = radar_driver_.acquire_s21_sweep();
|
||||
auto sweep = radar_driver_.acquire_sweep();
|
||||
validate_sweep(sweep);
|
||||
|
||||
ipc::SweepTraceBlock trace{};
|
||||
trace.combo = combo;
|
||||
trace.frequency_hz = std::move(sweep.frequency_hz);
|
||||
trace.s11 = std::move(sweep.s11);
|
||||
trace.s21 = std::move(sweep.s21);
|
||||
collection.traces.push_back(std::move(trace));
|
||||
}
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
#include "sweep_plan.hpp"
|
||||
|
||||
#include <stdexcept>
|
||||
|
||||
namespace radar::acq {
|
||||
|
||||
auto build_sweep_plan(const config::RunConfig& config) -> SweepPlan {
|
||||
// RunConfig is already validated in common_cpp/config. Keep this function
|
||||
// focused on plan construction only.
|
||||
if (config.run_combos.empty()) {
|
||||
throw std::runtime_error("run.combos must not be empty");
|
||||
}
|
||||
|
||||
SweepPlan plan{};
|
||||
plan.ordered_combos = config.run_combos;
|
||||
return plan;
|
||||
}
|
||||
|
||||
} // namespace radar::acq
|
||||
Reference in New Issue
Block a user