added s11 collections

This commit is contained in:
Ayzen
2026-03-26 15:48:16 +03:00
parent 9581730e41
commit 24f7ebb2fb
22 changed files with 184 additions and 859 deletions
@@ -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
@@ -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;
@@ -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
@@ -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