Refactor radar system
This commit is contained in:
@@ -35,18 +35,6 @@ class RadarDriver {
|
||||
/** @brief Acquire one sweep containing the forward traces exposed by the driver. */
|
||||
[[nodiscard]] virtual auto acquire_sweep() -> SweepTrace = 0;
|
||||
|
||||
/**
|
||||
* @brief Announce which switch combo the next `acquire_sweep` belongs to.
|
||||
*
|
||||
* Real radars are agnostic to this because the switch state itself decides
|
||||
* what they see. Mock drivers use it to synthesise per-combo variation so
|
||||
* downstream plots show eight distinct traces for an eight-combo run
|
||||
* instead of eight identical curves stacked on top of each other.
|
||||
*
|
||||
* Default implementation is a no-op so production drivers do not need to
|
||||
* override.
|
||||
*/
|
||||
virtual void set_active_combo(const ipc::ComboKey& /*combo*/) {}
|
||||
};
|
||||
|
||||
} // namespace radar::drivers
|
||||
|
||||
-414
@@ -1,414 +0,0 @@
|
||||
#include "../compact_m_k209_driver.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cctype>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
#include <utility>
|
||||
|
||||
namespace radar::drivers {
|
||||
namespace {
|
||||
|
||||
constexpr std::uint32_t kFloatBytes = 4U;
|
||||
constexpr std::uint32_t kComplexScalarCount = 2U;
|
||||
constexpr std::uint64_t kMaxBinaryBlockBytes = 512ULL * 1024ULL * 1024ULL;
|
||||
|
||||
[[nodiscard]] auto visa_resource(const std::string& resource) -> ViRsrc {
|
||||
return reinterpret_cast<ViRsrc>(const_cast<char*>(resource.c_str()));
|
||||
}
|
||||
|
||||
[[nodiscard]] auto visa_buffer(std::uint8_t* data) -> ViBuf {
|
||||
return reinterpret_cast<ViBuf>(data);
|
||||
}
|
||||
|
||||
[[nodiscard]] auto visa_const_buffer(const char* data) -> ViBuf {
|
||||
return reinterpret_cast<ViBuf>(const_cast<char*>(data));
|
||||
}
|
||||
|
||||
[[nodiscard]] auto is_line_ending(std::uint8_t value) -> bool {
|
||||
return value == static_cast<std::uint8_t>('\n') || value == static_cast<std::uint8_t>('\r');
|
||||
}
|
||||
|
||||
[[nodiscard]] auto is_response_separator(std::uint8_t value) -> bool {
|
||||
return is_line_ending(value) || value == static_cast<std::uint8_t>(';');
|
||||
}
|
||||
|
||||
[[nodiscard]] auto checked_vi_count(std::uint64_t value) -> ViUInt32 {
|
||||
const auto max_count = static_cast<std::uint64_t>(std::numeric_limits<ViUInt32>::max());
|
||||
return static_cast<ViUInt32>(std::min(value, max_count));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
CompactMK209Driver::CompactMK209Driver(CompactMK209DriverSettings settings) : settings_(std::move(settings)) {}
|
||||
|
||||
CompactMK209Driver::~CompactMK209Driver() {
|
||||
try {
|
||||
close();
|
||||
} catch (...) {
|
||||
}
|
||||
}
|
||||
|
||||
void CompactMK209Driver::open() {
|
||||
if (is_open_) {
|
||||
return;
|
||||
}
|
||||
if (settings_.resource.empty()) {
|
||||
throw std::runtime_error("K209 VISA resource must not be empty");
|
||||
}
|
||||
if (settings_.sweep.points < 2U) {
|
||||
throw std::runtime_error("K209 sweep points must be >= 2");
|
||||
}
|
||||
if (settings_.sweep.stop_hz < settings_.sweep.start_hz) {
|
||||
throw std::runtime_error("K209 sweep stop_hz must be >= start_hz");
|
||||
}
|
||||
if (!(settings_.sweep.if_bandwidth_hz > 0.0F)) {
|
||||
throw std::runtime_error("K209 IF bandwidth must be > 0");
|
||||
}
|
||||
|
||||
ViStatus status = viOpenDefaultRM(&resource_manager_);
|
||||
if (status < VI_SUCCESS) {
|
||||
throw std::runtime_error("Failed to open VISA resource manager: status=" + std::to_string(status));
|
||||
}
|
||||
|
||||
try {
|
||||
check_status(
|
||||
viOpen(resource_manager_, visa_resource(settings_.resource), VI_NULL, VI_NULL, &instrument_),
|
||||
"open K209 VISA resource"
|
||||
);
|
||||
check_status(
|
||||
viSetAttribute(instrument_, VI_ATTR_TMO_VALUE, static_cast<ViAttrState>(settings_.timeout_ms)),
|
||||
"set K209 VISA timeout"
|
||||
);
|
||||
check_status(
|
||||
viSetAttribute(instrument_, VI_ATTR_TERMCHAR_EN, static_cast<ViAttrState>(VI_FALSE)),
|
||||
"disable K209 VISA termchar"
|
||||
);
|
||||
write_command("*CLS");
|
||||
configure_device();
|
||||
} catch (...) {
|
||||
close();
|
||||
throw;
|
||||
}
|
||||
|
||||
is_open_ = true;
|
||||
}
|
||||
|
||||
void CompactMK209Driver::close() {
|
||||
if (instrument_ != VI_NULL) {
|
||||
viClose(instrument_);
|
||||
instrument_ = VI_NULL;
|
||||
}
|
||||
if (resource_manager_ != VI_NULL) {
|
||||
viClose(resource_manager_);
|
||||
resource_manager_ = VI_NULL;
|
||||
}
|
||||
is_open_ = false;
|
||||
}
|
||||
|
||||
auto CompactMK209Driver::acquire_sweep() -> SweepTrace {
|
||||
require_open();
|
||||
|
||||
auto traces = query_sweep_trace_pair(settings_.sweep.points);
|
||||
|
||||
SweepTrace trace{};
|
||||
trace.frequency_hz = frequency_hz_;
|
||||
trace.s11 = complex_trace_from_interleaved(traces.first);
|
||||
trace.s21 = complex_trace_from_interleaved(traces.second);
|
||||
return trace;
|
||||
}
|
||||
|
||||
auto CompactMK209Driver::query_identity() -> std::string {
|
||||
require_open();
|
||||
return trim_ascii(query_string("*IDN?"));
|
||||
}
|
||||
|
||||
auto CompactMK209Driver::query_system_error() -> std::string {
|
||||
require_open();
|
||||
return trim_ascii(query_string("SYST:ERR?"));
|
||||
}
|
||||
|
||||
void CompactMK209Driver::configure_device() {
|
||||
if (settings_.preset_on_open) {
|
||||
write_command("SYST:PRES");
|
||||
expect_operation_complete("*OPC?", "wait for K209 preset");
|
||||
}
|
||||
|
||||
write_command("SENS:FREQ:STAR " + format_frequency(settings_.sweep.start_hz));
|
||||
write_command("SENS:FREQ:STOP " + format_frequency(settings_.sweep.stop_hz));
|
||||
write_command("SENS:SWE:POIN " + std::to_string(settings_.sweep.points));
|
||||
write_command("SENS:SWE:POIN:TIME 0");
|
||||
write_command("SENS:BAND " + format_frequency(settings_.sweep.if_bandwidth_hz));
|
||||
write_command("SOUR:POW " + format_power(settings_.sweep.power_dbm));
|
||||
write_command("SENS:AVER OFF");
|
||||
write_command("CALC:PAR1:DEF S21");
|
||||
write_command("CALC:PAR1:SEL");
|
||||
write_command("FORM:DATA REAL32");
|
||||
write_command("FORM:BORD SWAP");
|
||||
write_command("INIT:CONT ON");
|
||||
write_command("TRIG:SOUR BUS");
|
||||
expect_operation_complete("*OPC?", "wait for K209 setup");
|
||||
|
||||
frequency_hz_ = query_float_array("SENS:FREQ:DATA?", settings_.sweep.points);
|
||||
}
|
||||
|
||||
void CompactMK209Driver::expect_operation_complete(const std::string& command, const std::string& context) {
|
||||
const auto response = trim_ascii(query_string(command));
|
||||
if (response != "1") {
|
||||
throw std::runtime_error(context + " returned unexpected *OPC? response: " + response);
|
||||
}
|
||||
}
|
||||
|
||||
void CompactMK209Driver::write_command(const std::string& command) {
|
||||
require_open();
|
||||
|
||||
const std::string message = command + "\n";
|
||||
ViUInt32 written = 0;
|
||||
check_status(
|
||||
viWrite(instrument_, visa_const_buffer(message.data()), static_cast<ViUInt32>(message.size()), &written),
|
||||
"write K209 command: " + command
|
||||
);
|
||||
if (written != message.size()) {
|
||||
throw std::runtime_error("Incomplete K209 command write: " + command);
|
||||
}
|
||||
}
|
||||
|
||||
auto CompactMK209Driver::query_string(const std::string& command) -> std::string {
|
||||
write_command(command);
|
||||
return read_line();
|
||||
}
|
||||
|
||||
auto CompactMK209Driver::query_float_array(
|
||||
const std::string& command,
|
||||
std::uint32_t expected_values
|
||||
) -> std::vector<float> {
|
||||
write_command(command);
|
||||
return read_float_array_response(command, expected_values);
|
||||
}
|
||||
|
||||
auto CompactMK209Driver::query_sweep_trace_pair(std::uint32_t points)
|
||||
-> std::pair<std::vector<float>, std::vector<float>> {
|
||||
write_command("TRIG:SING;*OPC?;:SENS:DATA:CORR? S11;:SENS:DATA:CORR? S21");
|
||||
const auto opc_response = read_ascii_token();
|
||||
if (opc_response != "1") {
|
||||
throw std::runtime_error("K209 sweep returned unexpected *OPC? response: " + opc_response);
|
||||
}
|
||||
|
||||
return {
|
||||
read_float_array_response("SENS:DATA:CORR? S11", points * kComplexScalarCount),
|
||||
read_float_array_response("SENS:DATA:CORR? S21", points * kComplexScalarCount),
|
||||
};
|
||||
}
|
||||
|
||||
auto CompactMK209Driver::read_float_array_response(
|
||||
const std::string& context,
|
||||
std::uint32_t expected_values
|
||||
) -> std::vector<float> {
|
||||
auto values = float_array_from_little_endian(read_ieee_block());
|
||||
if (values.size() != expected_values) {
|
||||
throw std::runtime_error(
|
||||
"K209 response for " + context + " returned " + std::to_string(values.size()) +
|
||||
" float32 values, expected " + std::to_string(expected_values)
|
||||
);
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
auto CompactMK209Driver::read_ascii_token() -> std::string {
|
||||
require_open();
|
||||
|
||||
std::string token{};
|
||||
token.reserve(32);
|
||||
|
||||
while (true) {
|
||||
const auto byte = read_byte();
|
||||
if (is_response_separator(byte)) {
|
||||
if (!token.empty()) {
|
||||
return token;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
token.push_back(static_cast<char>(byte));
|
||||
if (token.size() > 64U * 1024U) {
|
||||
throw std::runtime_error("K209 ASCII response token is too long");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto CompactMK209Driver::read_line() -> std::string {
|
||||
require_open();
|
||||
|
||||
std::string line{};
|
||||
line.reserve(128);
|
||||
bool has_content = false;
|
||||
|
||||
while (true) {
|
||||
const auto byte = read_byte();
|
||||
if (!has_content && is_line_ending(byte)) {
|
||||
continue;
|
||||
}
|
||||
if (byte == static_cast<std::uint8_t>('\n')) {
|
||||
break;
|
||||
}
|
||||
if (byte != static_cast<std::uint8_t>('\r')) {
|
||||
line.push_back(static_cast<char>(byte));
|
||||
has_content = true;
|
||||
}
|
||||
if (line.size() > 64U * 1024U) {
|
||||
throw std::runtime_error("K209 ASCII response line is too long");
|
||||
}
|
||||
}
|
||||
|
||||
return line;
|
||||
}
|
||||
|
||||
auto CompactMK209Driver::read_byte() -> std::uint8_t {
|
||||
auto bytes = read_exact(1);
|
||||
return bytes[0];
|
||||
}
|
||||
|
||||
auto CompactMK209Driver::read_exact(std::uint64_t size) -> std::vector<std::uint8_t> {
|
||||
require_open();
|
||||
|
||||
std::vector<std::uint8_t> bytes(size);
|
||||
std::uint64_t offset = 0;
|
||||
while (offset < size) {
|
||||
ViUInt32 transferred = 0;
|
||||
const auto count = checked_vi_count(size - offset);
|
||||
check_status(
|
||||
viRead(instrument_, visa_buffer(bytes.data() + offset), count, &transferred),
|
||||
"read K209 response bytes"
|
||||
);
|
||||
if (transferred == 0U) {
|
||||
throw std::runtime_error("K209 VISA read returned zero bytes before expected payload was complete");
|
||||
}
|
||||
offset += transferred;
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
auto CompactMK209Driver::read_ieee_block() -> std::vector<std::uint8_t> {
|
||||
std::uint8_t marker = read_byte();
|
||||
while (is_response_separator(marker)) {
|
||||
marker = read_byte();
|
||||
}
|
||||
if (marker != static_cast<std::uint8_t>('#')) {
|
||||
throw std::runtime_error("K209 binary response does not start with IEEE block marker '#'");
|
||||
}
|
||||
|
||||
const auto digit = read_byte();
|
||||
if (digit != static_cast<std::uint8_t>('8')) {
|
||||
throw std::runtime_error("K209 binary response uses unsupported IEEE block header width");
|
||||
}
|
||||
|
||||
const auto size_text_bytes = read_exact(8);
|
||||
std::uint64_t payload_size = 0;
|
||||
for (const auto byte : size_text_bytes) {
|
||||
if (!std::isdigit(static_cast<unsigned char>(byte))) {
|
||||
throw std::runtime_error("K209 binary response has non-numeric payload size");
|
||||
}
|
||||
payload_size = (payload_size * 10ULL) + static_cast<std::uint64_t>(byte - static_cast<std::uint8_t>('0'));
|
||||
}
|
||||
if (payload_size == 0U || payload_size > kMaxBinaryBlockBytes) {
|
||||
throw std::runtime_error("K209 binary response payload size is out of allowed range");
|
||||
}
|
||||
if ((payload_size % kFloatBytes) != 0U) {
|
||||
throw std::runtime_error("K209 binary response payload size is not aligned to float32 values");
|
||||
}
|
||||
|
||||
return read_exact(payload_size);
|
||||
}
|
||||
|
||||
void CompactMK209Driver::check_status(ViStatus status, const std::string& context) const {
|
||||
if (status >= VI_SUCCESS) {
|
||||
return;
|
||||
}
|
||||
throw std::runtime_error(context + ": " + status_message(status));
|
||||
}
|
||||
|
||||
auto CompactMK209Driver::status_message(ViStatus status) const -> std::string {
|
||||
std::array<ViChar, 256> description{};
|
||||
const auto session = instrument_ != VI_NULL ? instrument_ : resource_manager_;
|
||||
if (session != VI_NULL && viStatusDesc(session, status, description.data()) >= VI_SUCCESS) {
|
||||
return std::string(description.data()) + " (status=" + std::to_string(status) + ")";
|
||||
}
|
||||
return "VISA status=" + std::to_string(status);
|
||||
}
|
||||
|
||||
void CompactMK209Driver::require_open() const {
|
||||
if (instrument_ == VI_NULL) {
|
||||
throw std::runtime_error("K209 VISA instrument is not open");
|
||||
}
|
||||
}
|
||||
|
||||
auto CompactMK209Driver::float_array_from_little_endian(std::vector<std::uint8_t> bytes) -> std::vector<float> {
|
||||
if ((bytes.size() % kFloatBytes) != 0U) {
|
||||
throw std::runtime_error("K209 binary float payload is not aligned to 4 bytes");
|
||||
}
|
||||
|
||||
std::vector<float> values(bytes.size() / kFloatBytes, 0.0F);
|
||||
for (std::size_t index = 0; index < values.size(); ++index) {
|
||||
const auto offset = index * kFloatBytes;
|
||||
const std::uint32_t bits = static_cast<std::uint32_t>(bytes[offset]) |
|
||||
(static_cast<std::uint32_t>(bytes[offset + 1U]) << 8U) |
|
||||
(static_cast<std::uint32_t>(bytes[offset + 2U]) << 16U) |
|
||||
(static_cast<std::uint32_t>(bytes[offset + 3U]) << 24U);
|
||||
std::memcpy(&values[index], &bits, sizeof(float));
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
auto CompactMK209Driver::complex_trace_from_interleaved(const std::vector<float>& values)
|
||||
-> std::vector<ipc::Complex32> {
|
||||
if ((values.size() % kComplexScalarCount) != 0U) {
|
||||
throw std::runtime_error("K209 complex trace payload has odd scalar count");
|
||||
}
|
||||
|
||||
std::vector<ipc::Complex32> trace{};
|
||||
trace.reserve(values.size() / kComplexScalarCount);
|
||||
for (std::size_t index = 0; index < values.size(); index += kComplexScalarCount) {
|
||||
trace.push_back(ipc::Complex32{.re = values[index], .im = values[index + 1U]});
|
||||
}
|
||||
return trace;
|
||||
}
|
||||
|
||||
auto CompactMK209Driver::format_frequency(float value_hz) -> std::string {
|
||||
if (!std::isfinite(value_hz)) {
|
||||
throw std::runtime_error("K209 frequency value is not finite");
|
||||
}
|
||||
|
||||
std::ostringstream stream;
|
||||
stream.precision(9);
|
||||
stream << std::fixed << value_hz;
|
||||
return stream.str();
|
||||
}
|
||||
|
||||
auto CompactMK209Driver::format_power(float value_dbm) -> std::string {
|
||||
if (!std::isfinite(value_dbm)) {
|
||||
throw std::runtime_error("K209 power value is not finite");
|
||||
}
|
||||
|
||||
std::ostringstream stream;
|
||||
stream.precision(3);
|
||||
stream << std::fixed << value_dbm;
|
||||
return stream.str();
|
||||
}
|
||||
|
||||
auto CompactMK209Driver::trim_ascii(std::string value) -> std::string {
|
||||
const auto first = std::find_if_not(value.begin(), value.end(), [](unsigned char ch) {
|
||||
return std::isspace(ch) != 0;
|
||||
});
|
||||
const auto last = std::find_if_not(value.rbegin(), value.rend(), [](unsigned char ch) {
|
||||
return std::isspace(ch) != 0;
|
||||
}).base();
|
||||
if (first >= last) {
|
||||
return {};
|
||||
}
|
||||
return std::string(first, last);
|
||||
}
|
||||
|
||||
} // namespace radar::drivers
|
||||
-71
@@ -1,71 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <visa.h>
|
||||
|
||||
#include "radar_driver.hpp"
|
||||
#include "run_config.hpp"
|
||||
|
||||
namespace radar::drivers {
|
||||
|
||||
struct CompactMK209DriverSettings {
|
||||
std::string resource{};
|
||||
config::RadarSweepSettings sweep{};
|
||||
std::uint32_t timeout_ms = 20'000;
|
||||
bool preset_on_open = true;
|
||||
};
|
||||
|
||||
class CompactMK209Driver final : public RadarDriver {
|
||||
public:
|
||||
explicit CompactMK209Driver(CompactMK209DriverSettings settings);
|
||||
~CompactMK209Driver() override;
|
||||
|
||||
void open() override;
|
||||
void close() override;
|
||||
[[nodiscard]] auto acquire_sweep() -> SweepTrace override;
|
||||
|
||||
[[nodiscard]] auto query_identity() -> std::string;
|
||||
[[nodiscard]] auto query_system_error() -> std::string;
|
||||
|
||||
private:
|
||||
void configure_device();
|
||||
void expect_operation_complete(const std::string& command, const std::string& context);
|
||||
|
||||
void write_command(const std::string& command);
|
||||
[[nodiscard]] auto query_string(const std::string& command) -> std::string;
|
||||
[[nodiscard]] auto query_float_array(const std::string& command, std::uint32_t expected_values)
|
||||
-> std::vector<float>;
|
||||
[[nodiscard]] auto query_sweep_trace_pair(std::uint32_t points)
|
||||
-> std::pair<std::vector<float>, std::vector<float>>;
|
||||
[[nodiscard]] auto read_float_array_response(const std::string& context, std::uint32_t expected_values)
|
||||
-> std::vector<float>;
|
||||
|
||||
[[nodiscard]] auto read_ascii_token() -> std::string;
|
||||
[[nodiscard]] auto read_line() -> std::string;
|
||||
[[nodiscard]] auto read_byte() -> std::uint8_t;
|
||||
[[nodiscard]] auto read_exact(std::uint64_t size) -> std::vector<std::uint8_t>;
|
||||
[[nodiscard]] auto read_ieee_block() -> std::vector<std::uint8_t>;
|
||||
|
||||
void check_status(ViStatus status, const std::string& context) const;
|
||||
[[nodiscard]] auto status_message(ViStatus status) const -> std::string;
|
||||
void require_open() const;
|
||||
|
||||
[[nodiscard]] static auto float_array_from_little_endian(std::vector<std::uint8_t> bytes) -> std::vector<float>;
|
||||
[[nodiscard]] static auto complex_trace_from_interleaved(const std::vector<float>& values)
|
||||
-> std::vector<ipc::Complex32>;
|
||||
[[nodiscard]] static auto format_frequency(float value_hz) -> std::string;
|
||||
[[nodiscard]] static auto format_power(float value_dbm) -> std::string;
|
||||
[[nodiscard]] static auto trim_ascii(std::string value) -> std::string;
|
||||
|
||||
CompactMK209DriverSettings settings_{};
|
||||
std::vector<float> frequency_hz_{};
|
||||
bool is_open_ = false;
|
||||
ViSession resource_manager_ = VI_NULL;
|
||||
ViSession instrument_ = VI_NULL;
|
||||
};
|
||||
|
||||
} // namespace radar::drivers
|
||||
+35
-203
@@ -3,15 +3,11 @@
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <complex>
|
||||
#include <cstdint>
|
||||
#include <exception>
|
||||
#include <random>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <stdexcept>
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
@@ -43,60 +39,6 @@ constexpr std::uint32_t kNativeAcquireMaxAttempts = 3U;
|
||||
);
|
||||
}
|
||||
|
||||
// One synthetic GPR reflector. `range_m` is its physical depth, `reflection`
|
||||
// is the dimensionless complex reflection coefficient (|Γ| ≤ 1).
|
||||
struct MockTarget {
|
||||
float range_m;
|
||||
float reflection_magnitude;
|
||||
};
|
||||
|
||||
// Three reflectors at GPR-typical depths: a strong near-surface scatterer,
|
||||
// a mid-depth target, and a weak deeper one. The magnitudes are tuned so the
|
||||
// summed S21 stays within unit modulus across the sweep band.
|
||||
constexpr std::array<MockTarget, 3> kMockTargets = {{
|
||||
{0.45F, 0.55F},
|
||||
{1.30F, 0.30F},
|
||||
{2.90F, 0.18F},
|
||||
}};
|
||||
|
||||
// Group velocity in moderately wet soil (≈ c / sqrt(εr), εr ≈ 4). The choice
|
||||
// is what maps reflector depth to round-trip phase delay; it is held constant
|
||||
// to keep the simulator deterministic.
|
||||
constexpr float kGroundVelocityMps = 1.5e8F;
|
||||
|
||||
// Soil attenuation grows with frequency. Calibrated so a target at 3 m sees
|
||||
// roughly −20 dB extra loss at 6 GHz on top of geometric spreading.
|
||||
constexpr float kAttenuationCoeffPerMeterAtRefHz = 0.22F;
|
||||
constexpr float kAttenuationReferenceHz = 6e9F;
|
||||
|
||||
// Antenna mismatch dominates S11: simulate one shallow reflection right at
|
||||
// the connector, plus a small amount of cross-coupling from S21 targets.
|
||||
constexpr float kS11ConnectorReflection = 0.55F;
|
||||
constexpr float kS11ConnectorRangeM = 0.02F;
|
||||
constexpr float kS11CrossCouplingFactor = 0.06F;
|
||||
|
||||
// Noise floor in linear voltage units. Real LibreVNA hits ~ −90 dB at 1 kHz
|
||||
// IFBW; pick something a touch noisier so the GPR processor has to work.
|
||||
constexpr float kNoiseAmplitudeLinear = 0.004F;
|
||||
|
||||
// Minimum simulated dwell so 0-point or pathological configs don't busy-loop.
|
||||
constexpr auto kMockMinimumSweepDuration = std::chrono::microseconds(50);
|
||||
|
||||
// Compute the dwell time the mock pretends to spend on the device. Mirrors
|
||||
// the real LibreVNA contract: per-point dwell ≈ 1 / IFBW. Capped so absurd
|
||||
// configs (IFBW ≈ 0 or huge sweeps) cannot freeze the producer for hours.
|
||||
[[nodiscard]] auto mock_target_sweep_duration(const config::RadarSweepSettings& sweep)
|
||||
-> std::chrono::nanoseconds {
|
||||
const float points = std::max(1.0F, static_cast<float>(sweep.points));
|
||||
const float if_bw = std::max(1.0F, sweep.if_bandwidth_hz);
|
||||
const double seconds = static_cast<double>(points) / static_cast<double>(if_bw);
|
||||
const auto duration_ns = std::chrono::nanoseconds(
|
||||
static_cast<std::chrono::nanoseconds::rep>(seconds * 1e9)
|
||||
);
|
||||
constexpr auto kHardCap = std::chrono::seconds(5);
|
||||
return std::clamp<std::chrono::nanoseconds>(duration_ns, kMockMinimumSweepDuration, kHardCap);
|
||||
}
|
||||
|
||||
[[nodiscard]] auto is_retryable_native_acquire_error(std::string_view message) -> bool {
|
||||
// A stop request must not be retried: drain to the caller immediately (#26).
|
||||
if (message.find("aborted by stop request") != std::string_view::npos) {
|
||||
@@ -139,9 +81,7 @@ void LibreVnaMinimalDriver::open() {
|
||||
throw std::runtime_error("Radar sweep points must be > 0");
|
||||
}
|
||||
|
||||
if (settings_.mode == config::DriverMode::Native) {
|
||||
open_native();
|
||||
}
|
||||
open_native();
|
||||
|
||||
is_open_ = true;
|
||||
}
|
||||
@@ -151,166 +91,58 @@ void LibreVnaMinimalDriver::close() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (settings_.mode == config::DriverMode::Native) {
|
||||
close_native();
|
||||
}
|
||||
close_native();
|
||||
|
||||
is_open_ = false;
|
||||
}
|
||||
|
||||
void LibreVnaMinimalDriver::set_active_combo(const ipc::ComboKey& combo) {
|
||||
active_combo_ = combo;
|
||||
}
|
||||
|
||||
auto LibreVnaMinimalDriver::acquire_sweep() -> SweepTrace {
|
||||
if (!is_open_) {
|
||||
throw std::runtime_error("Radar driver is not open");
|
||||
}
|
||||
|
||||
++sweep_index_;
|
||||
switch (settings_.mode) {
|
||||
case config::DriverMode::Mock:
|
||||
return acquire_mock();
|
||||
case config::DriverMode::Native: {
|
||||
std::exception_ptr last_exception{};
|
||||
std::string last_message{};
|
||||
std::exception_ptr last_exception{};
|
||||
std::string last_message{};
|
||||
|
||||
for (std::uint32_t attempt = 1; attempt <= kNativeAcquireMaxAttempts; ++attempt) {
|
||||
try {
|
||||
return acquire_native();
|
||||
} catch (const std::exception& exception) {
|
||||
last_exception = std::current_exception();
|
||||
last_message = exception.what();
|
||||
for (std::uint32_t attempt = 1; attempt <= kNativeAcquireMaxAttempts; ++attempt) {
|
||||
try {
|
||||
return acquire_native();
|
||||
} catch (const std::exception& exception) {
|
||||
last_exception = std::current_exception();
|
||||
last_message = exception.what();
|
||||
|
||||
if (!is_retryable_native_acquire_error(last_message) || attempt == kNativeAcquireMaxAttempts) {
|
||||
break;
|
||||
}
|
||||
if (detail::native_stop_requested()) {
|
||||
break; // #26: do not waste reconnect attempts while shutting down.
|
||||
}
|
||||
|
||||
// #31: recover from transient USB/protocol stalls without
|
||||
// destroying the libusb_context. Release only the interface
|
||||
// and handle, then reopen against the surviving context so
|
||||
// we ride out a brief device re-enumeration instead of
|
||||
// re-initialising libusb from scratch on every retry.
|
||||
rx_buffer_.clear();
|
||||
packet_queue_.clear();
|
||||
if (usb_handle_ != nullptr && interface_claimed_) {
|
||||
libusb_release_interface(usb_handle_, detail::kUsbInterface);
|
||||
interface_claimed_ = false;
|
||||
}
|
||||
if (usb_handle_ != nullptr) {
|
||||
libusb_close(usb_handle_);
|
||||
usb_handle_ = nullptr;
|
||||
}
|
||||
protocol_version_ = 0;
|
||||
device_num_ports_ = 0;
|
||||
open_native(); // Reuses the surviving usb_context_ and retries discovery.
|
||||
}
|
||||
if (!is_retryable_native_acquire_error(last_message) || attempt == kNativeAcquireMaxAttempts) {
|
||||
break;
|
||||
}
|
||||
if (detail::native_stop_requested()) {
|
||||
break; // #26: do not waste reconnect attempts while shutting down.
|
||||
}
|
||||
|
||||
if (last_exception != nullptr) {
|
||||
std::rethrow_exception(last_exception);
|
||||
// #31: recover from transient USB/protocol stalls without
|
||||
// destroying the libusb_context. Release only the interface
|
||||
// and handle, then reopen against the surviving context so
|
||||
// we ride out a brief device re-enumeration instead of
|
||||
// re-initialising libusb from scratch on every retry.
|
||||
rx_buffer_.clear();
|
||||
packet_queue_.clear();
|
||||
if (usb_handle_ != nullptr && interface_claimed_) {
|
||||
libusb_release_interface(usb_handle_, detail::kUsbInterface);
|
||||
interface_claimed_ = false;
|
||||
}
|
||||
throw std::runtime_error("Native acquisition failed without exception detail");
|
||||
if (usb_handle_ != nullptr) {
|
||||
libusb_close(usb_handle_);
|
||||
usb_handle_ = nullptr;
|
||||
}
|
||||
protocol_version_ = 0;
|
||||
device_num_ports_ = 0;
|
||||
open_native(); // Reuses the surviving usb_context_ and retries discovery.
|
||||
}
|
||||
default:
|
||||
throw std::runtime_error("Unsupported radar driver mode");
|
||||
}
|
||||
}
|
||||
|
||||
auto LibreVnaMinimalDriver::acquire_mock() -> SweepTrace {
|
||||
// Synthesise a frequency-domain VNA response containing several discrete
|
||||
// reflectors at known depths, plus light Gaussian noise. The sweep dwells
|
||||
// for a realistic duration derived from the configured IF bandwidth so
|
||||
// downstream stages cannot be flooded faster than a real device would
|
||||
// produce data.
|
||||
const auto started_at = std::chrono::steady_clock::now();
|
||||
const auto target_duration = mock_target_sweep_duration(settings_.sweep);
|
||||
|
||||
SweepTrace trace{};
|
||||
trace.frequency_hz.reserve(settings_.sweep.points);
|
||||
trace.s11.reserve(settings_.sweep.points);
|
||||
trace.s21.reserve(settings_.sweep.points);
|
||||
|
||||
const float span_hz = settings_.sweep.stop_hz - settings_.sweep.start_hz;
|
||||
const float denominator =
|
||||
settings_.sweep.points > 1U ? static_cast<float>(settings_.sweep.points - 1U) : 1.0F;
|
||||
|
||||
// A tiny per-sweep range drift gives the rendered B-scan a visible motion
|
||||
// signature so the simulator does not look frozen.
|
||||
const float range_drift_m =
|
||||
0.01F * std::sin(0.07F * static_cast<float>(sweep_index_));
|
||||
|
||||
// Combo-dependent variation. Without this the mock returns near-identical
|
||||
// S21 for every (input, output) combo and an eight-combo pass-through plot
|
||||
// collapses into a single visible trace. The factors below are arbitrary
|
||||
// but chosen small enough that the overall response stays in a reasonable
|
||||
// band and large enough that each pair is visually distinct.
|
||||
const auto input_pos = static_cast<float>(active_combo_.input_pos);
|
||||
const auto output_pos = static_cast<float>(active_combo_.output_pos);
|
||||
const float combo_amplitude_gain = 0.55F + 0.08F * input_pos + 0.05F * output_pos;
|
||||
const float combo_phase_offset = 0.4F * input_pos + 0.9F * output_pos;
|
||||
const float combo_range_offset_m = 0.05F * input_pos + 0.12F * output_pos;
|
||||
|
||||
// Deterministic-per-sweep noise so two consecutive frames look distinct
|
||||
// but the test stays reproducible for any given sweep index.
|
||||
std::mt19937 noise_engine(
|
||||
static_cast<std::uint32_t>(0x9E3779B9ULL ^ sweep_index_)
|
||||
);
|
||||
std::normal_distribution<float> noise_dist(0.0F, kNoiseAmplitudeLinear);
|
||||
|
||||
for (std::uint32_t point = 0; point < settings_.sweep.points; ++point) {
|
||||
const float ratio = static_cast<float>(point) / denominator;
|
||||
const float frequency_hz = settings_.sweep.start_hz + span_hz * ratio;
|
||||
const float frequency_scale = frequency_hz / kAttenuationReferenceHz;
|
||||
|
||||
std::complex<float> s21_total{0.0F, 0.0F};
|
||||
std::complex<float> s11_total{0.0F, 0.0F};
|
||||
|
||||
for (const auto& target : kMockTargets) {
|
||||
const float range_m = target.range_m + range_drift_m + combo_range_offset_m;
|
||||
// Round-trip phase: 2π·f·(2R/v).
|
||||
const float round_trip_phase =
|
||||
2.0F * detail::kPi * frequency_hz * (2.0F * range_m / kGroundVelocityMps);
|
||||
// Geometric spreading: 1/(1 + R) keeps near-zero ranges finite.
|
||||
const float spreading = 1.0F / (1.0F + range_m);
|
||||
// Frequency-dependent soil attenuation in linear amplitude.
|
||||
const float attenuation =
|
||||
std::exp(-kAttenuationCoeffPerMeterAtRefHz * range_m * frequency_scale);
|
||||
|
||||
const std::complex<float> contribution = std::polar<float>(
|
||||
target.reflection_magnitude * spreading * attenuation * combo_amplitude_gain,
|
||||
-round_trip_phase + combo_phase_offset
|
||||
);
|
||||
s21_total += contribution;
|
||||
s11_total += kS11CrossCouplingFactor * contribution;
|
||||
}
|
||||
|
||||
// Antenna mismatch dominates the near-field S11 response.
|
||||
const float antenna_phase =
|
||||
2.0F * detail::kPi * frequency_hz * (2.0F * kS11ConnectorRangeM / kGroundVelocityMps);
|
||||
s11_total += std::polar<float>(kS11ConnectorReflection, -antenna_phase);
|
||||
|
||||
// Independent noise per channel; complex variance ≈ kNoiseAmplitudeLinear².
|
||||
s21_total += std::complex<float>(noise_dist(noise_engine), noise_dist(noise_engine));
|
||||
s11_total += std::complex<float>(noise_dist(noise_engine), noise_dist(noise_engine));
|
||||
|
||||
trace.frequency_hz.push_back(frequency_hz);
|
||||
trace.s11.push_back({.re = s11_total.real(), .im = s11_total.imag()});
|
||||
trace.s21.push_back({.re = s21_total.real(), .im = s21_total.imag()});
|
||||
}
|
||||
|
||||
// Honour the IFBW-derived dwell time. If generation already took longer
|
||||
// than the simulated device would have needed (huge `points` × CPU jitter)
|
||||
// we skip the sleep so the producer does not fall further behind.
|
||||
const auto elapsed = std::chrono::steady_clock::now() - started_at;
|
||||
if (elapsed < target_duration) {
|
||||
std::this_thread::sleep_for(target_duration - elapsed);
|
||||
if (last_exception != nullptr) {
|
||||
std::rethrow_exception(last_exception);
|
||||
}
|
||||
|
||||
return trace;
|
||||
throw std::runtime_error("Native acquisition failed without exception detail");
|
||||
}
|
||||
|
||||
auto LibreVnaMinimalDriver::acquire_native() -> SweepTrace {
|
||||
|
||||
-9
@@ -23,10 +23,8 @@ namespace radar::drivers {
|
||||
* @brief Runtime settings for minimal LibreVNA acquisition.
|
||||
*/
|
||||
struct LibreVnaMinimalDriverSettings {
|
||||
config::DriverMode mode = config::DriverMode::Mock;
|
||||
std::string serial{};
|
||||
config::RadarSweepSettings sweep{};
|
||||
float mock_signal_hz = 5'000'000.0F;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -43,7 +41,6 @@ class LibreVnaMinimalDriver final : public RadarDriver {
|
||||
void open() override;
|
||||
void close() override;
|
||||
[[nodiscard]] auto acquire_sweep() -> SweepTrace override;
|
||||
void set_active_combo(const ipc::ComboKey& combo) override;
|
||||
|
||||
private:
|
||||
/**
|
||||
@@ -66,7 +63,6 @@ class LibreVnaMinimalDriver final : public RadarDriver {
|
||||
bool has_s21 = false;
|
||||
};
|
||||
|
||||
[[nodiscard]] auto acquire_mock() -> SweepTrace;
|
||||
[[nodiscard]] auto acquire_native() -> SweepTrace;
|
||||
|
||||
void open_native();
|
||||
@@ -92,11 +88,6 @@ class LibreVnaMinimalDriver final : public RadarDriver {
|
||||
|
||||
LibreVnaMinimalDriverSettings settings_{};
|
||||
bool is_open_ = false;
|
||||
std::uint64_t sweep_index_ = 0;
|
||||
// Latest combo announced by the orchestrator. Used by the mock backend to
|
||||
// give each (input, output) pair a slightly different reflectivity profile
|
||||
// so a multi-combo run does not render as eight identical traces.
|
||||
ipc::ComboKey active_combo_{};
|
||||
|
||||
libusb_context* usb_context_ = nullptr;
|
||||
libusb_device_handle* usb_handle_ = nullptr;
|
||||
|
||||
-271
@@ -1,271 +0,0 @@
|
||||
#include "../remote_compact_m_k209_driver.hpp"
|
||||
|
||||
#include <arpa/inet.h>
|
||||
#include <netdb.h>
|
||||
#include <netinet/tcp.h>
|
||||
#include <sys/socket.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <cerrno>
|
||||
#include <cstring>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace radar::drivers {
|
||||
namespace {
|
||||
|
||||
constexpr std::uint8_t kCommandConfigure = static_cast<std::uint8_t>('C');
|
||||
constexpr std::uint8_t kCommandAcquire = static_cast<std::uint8_t>('A');
|
||||
constexpr std::uint8_t kStatusOk = static_cast<std::uint8_t>('O');
|
||||
constexpr std::uint8_t kStatusError = static_cast<std::uint8_t>('E');
|
||||
|
||||
[[nodiscard]] auto system_error_message(const std::string& context) -> std::string {
|
||||
return context + ": " + std::strerror(errno);
|
||||
}
|
||||
|
||||
void append_u32_be(std::vector<std::uint8_t>& buffer, std::uint32_t value) {
|
||||
buffer.push_back(static_cast<std::uint8_t>((value >> 24U) & 0xFFU));
|
||||
buffer.push_back(static_cast<std::uint8_t>((value >> 16U) & 0xFFU));
|
||||
buffer.push_back(static_cast<std::uint8_t>((value >> 8U) & 0xFFU));
|
||||
buffer.push_back(static_cast<std::uint8_t>(value & 0xFFU));
|
||||
}
|
||||
|
||||
void append_u64_be(std::vector<std::uint8_t>& buffer, std::uint64_t value) {
|
||||
for (int shift = 56; shift >= 0; shift -= 8) {
|
||||
buffer.push_back(static_cast<std::uint8_t>((value >> static_cast<unsigned>(shift)) & 0xFFU));
|
||||
}
|
||||
}
|
||||
|
||||
void append_double_be(std::vector<std::uint8_t>& buffer, double value) {
|
||||
std::uint64_t bits = 0;
|
||||
static_assert(sizeof(bits) == sizeof(value));
|
||||
std::memcpy(&bits, &value, sizeof(bits));
|
||||
append_u64_be(buffer, bits);
|
||||
}
|
||||
|
||||
[[nodiscard]] auto parse_u32_be(const std::uint8_t* data) -> std::uint32_t {
|
||||
return (static_cast<std::uint32_t>(data[0]) << 24U) |
|
||||
(static_cast<std::uint32_t>(data[1]) << 16U) |
|
||||
(static_cast<std::uint32_t>(data[2]) << 8U) |
|
||||
static_cast<std::uint32_t>(data[3]);
|
||||
}
|
||||
|
||||
void send_all(int socket_fd, const void* data, std::size_t size) {
|
||||
const auto* cursor = static_cast<const std::uint8_t*>(data);
|
||||
auto remaining = size;
|
||||
while (remaining > 0U) {
|
||||
const auto sent = ::send(socket_fd, cursor, remaining, MSG_NOSIGNAL);
|
||||
if (sent < 0) {
|
||||
throw std::runtime_error(system_error_message("send to K209 remote server failed"));
|
||||
}
|
||||
if (sent == 0) {
|
||||
throw std::runtime_error("send to K209 remote server returned zero bytes");
|
||||
}
|
||||
cursor += sent;
|
||||
remaining -= static_cast<std::size_t>(sent);
|
||||
}
|
||||
}
|
||||
|
||||
void recv_exact(int socket_fd, void* data, std::size_t size) {
|
||||
auto* cursor = static_cast<std::uint8_t*>(data);
|
||||
auto remaining = size;
|
||||
while (remaining > 0U) {
|
||||
const auto received = ::recv(socket_fd, cursor, remaining, 0);
|
||||
if (received < 0) {
|
||||
throw std::runtime_error(system_error_message("read from K209 remote server failed"));
|
||||
}
|
||||
if (received == 0) {
|
||||
throw std::runtime_error("K209 remote server closed the connection");
|
||||
}
|
||||
cursor += received;
|
||||
remaining -= static_cast<std::size_t>(received);
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] auto recv_u32(int socket_fd) -> std::uint32_t {
|
||||
std::uint8_t bytes[4]{};
|
||||
recv_exact(socket_fd, bytes, sizeof(bytes));
|
||||
return parse_u32_be(bytes);
|
||||
}
|
||||
|
||||
void read_status(int socket_fd) {
|
||||
std::uint8_t status = 0;
|
||||
recv_exact(socket_fd, &status, 1);
|
||||
if (status == kStatusOk) {
|
||||
return;
|
||||
}
|
||||
if (status == kStatusError) {
|
||||
const auto message_size = recv_u32(socket_fd);
|
||||
std::string message(message_size, '\0');
|
||||
if (message_size > 0U) {
|
||||
recv_exact(socket_fd, message.data(), message.size());
|
||||
}
|
||||
throw std::runtime_error("K209 remote server error: " + message);
|
||||
}
|
||||
throw std::runtime_error("K209 remote server returned invalid status byte");
|
||||
}
|
||||
|
||||
[[nodiscard]] auto recv_float32_array(int socket_fd, std::uint32_t expected_values, const std::string& context)
|
||||
-> std::vector<float> {
|
||||
const auto payload_size = recv_u32(socket_fd);
|
||||
const auto expected_size = expected_values * static_cast<std::uint32_t>(sizeof(float));
|
||||
if (payload_size != expected_size) {
|
||||
throw std::runtime_error(
|
||||
"K209 remote " + context + " payload has " + std::to_string(payload_size) +
|
||||
" bytes, expected " + std::to_string(expected_size)
|
||||
);
|
||||
}
|
||||
|
||||
std::vector<float> values(expected_values);
|
||||
if (payload_size > 0U) {
|
||||
recv_exact(socket_fd, values.data(), payload_size);
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
[[nodiscard]] auto connect_socket(const std::string& host, std::uint32_t port, std::uint32_t timeout_ms) -> int {
|
||||
addrinfo hints{};
|
||||
hints.ai_family = AF_UNSPEC;
|
||||
hints.ai_socktype = SOCK_STREAM;
|
||||
hints.ai_protocol = IPPROTO_TCP;
|
||||
|
||||
addrinfo* result = nullptr;
|
||||
const auto port_text = std::to_string(port);
|
||||
const auto rc = ::getaddrinfo(host.c_str(), port_text.c_str(), &hints, &result);
|
||||
if (rc != 0) {
|
||||
throw std::runtime_error("getaddrinfo failed for K209 remote host " + host + ": " + ::gai_strerror(rc));
|
||||
}
|
||||
|
||||
int connected_fd = -1;
|
||||
std::string last_error;
|
||||
for (auto* item = result; item != nullptr; item = item->ai_next) {
|
||||
const int fd = ::socket(item->ai_family, item->ai_socktype, item->ai_protocol);
|
||||
if (fd < 0) {
|
||||
last_error = system_error_message("socket");
|
||||
continue;
|
||||
}
|
||||
|
||||
const int one = 1;
|
||||
static_cast<void>(::setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &one, sizeof(one)));
|
||||
timeval timeout{};
|
||||
timeout.tv_sec = static_cast<long>(timeout_ms / 1000U);
|
||||
timeout.tv_usec = static_cast<long>((timeout_ms % 1000U) * 1000U);
|
||||
static_cast<void>(::setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)));
|
||||
static_cast<void>(::setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout)));
|
||||
|
||||
if (::connect(fd, item->ai_addr, item->ai_addrlen) == 0) {
|
||||
connected_fd = fd;
|
||||
break;
|
||||
}
|
||||
last_error = system_error_message("connect");
|
||||
::close(fd);
|
||||
}
|
||||
::freeaddrinfo(result);
|
||||
|
||||
if (connected_fd < 0) {
|
||||
throw std::runtime_error("Failed to connect to K209 remote server " + host + ":" + port_text + ": " + last_error);
|
||||
}
|
||||
return connected_fd;
|
||||
}
|
||||
|
||||
[[nodiscard]] auto complex_from_interleaved(const std::vector<float>& values, std::uint32_t points, const std::string& context)
|
||||
-> std::vector<ipc::Complex32> {
|
||||
if (values.size() != static_cast<std::size_t>(points) * 2U) {
|
||||
throw std::runtime_error("K209 remote " + context + " returned unexpected scalar count");
|
||||
}
|
||||
|
||||
std::vector<ipc::Complex32> output(points);
|
||||
for (std::uint32_t index = 0; index < points; ++index) {
|
||||
output[index] = ipc::Complex32{
|
||||
.re = values[static_cast<std::size_t>(index) * 2U],
|
||||
.im = values[static_cast<std::size_t>(index) * 2U + 1U],
|
||||
};
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
RemoteCompactMK209Driver::RemoteCompactMK209Driver(RemoteCompactMK209DriverSettings settings)
|
||||
: settings_(std::move(settings)) {
|
||||
if (settings_.host.empty()) {
|
||||
throw std::runtime_error("K209 remote host must not be empty");
|
||||
}
|
||||
if (settings_.port == 0U || settings_.port > 65535U) {
|
||||
throw std::runtime_error("K209 remote port must be in 1..65535");
|
||||
}
|
||||
if (settings_.timeout_ms == 0U) {
|
||||
throw std::runtime_error("K209 remote timeout_ms must be > 0");
|
||||
}
|
||||
}
|
||||
|
||||
RemoteCompactMK209Driver::~RemoteCompactMK209Driver() {
|
||||
try {
|
||||
close();
|
||||
} catch (...) {
|
||||
}
|
||||
}
|
||||
|
||||
void RemoteCompactMK209Driver::open() {
|
||||
if (socket_fd_ >= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
socket_fd_ = connect_socket(settings_.host, settings_.port, settings_.timeout_ms);
|
||||
|
||||
try {
|
||||
std::vector<std::uint8_t> payload{};
|
||||
payload.reserve(1U + 8U + 8U + 4U + 8U + 8U);
|
||||
payload.push_back(kCommandConfigure);
|
||||
append_double_be(payload, static_cast<double>(settings_.sweep.start_hz));
|
||||
append_double_be(payload, static_cast<double>(settings_.sweep.stop_hz));
|
||||
append_u32_be(payload, settings_.sweep.points);
|
||||
append_double_be(payload, static_cast<double>(settings_.sweep.if_bandwidth_hz));
|
||||
append_double_be(payload, static_cast<double>(settings_.sweep.power_dbm));
|
||||
send_all(socket_fd_, payload.data(), payload.size());
|
||||
|
||||
read_status(socket_fd_);
|
||||
const auto points = recv_u32(socket_fd_);
|
||||
if (points != settings_.sweep.points) {
|
||||
throw std::runtime_error("K209 remote server returned unexpected configured point count");
|
||||
}
|
||||
frequency_hz_ = recv_float32_array(socket_fd_, points, "frequency");
|
||||
} catch (...) {
|
||||
close();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
void RemoteCompactMK209Driver::close() {
|
||||
if (socket_fd_ >= 0) {
|
||||
::close(socket_fd_);
|
||||
socket_fd_ = -1;
|
||||
}
|
||||
frequency_hz_.clear();
|
||||
}
|
||||
|
||||
auto RemoteCompactMK209Driver::acquire_sweep() -> SweepTrace {
|
||||
if (socket_fd_ < 0) {
|
||||
throw std::runtime_error("K209 remote driver is not open");
|
||||
}
|
||||
const std::uint8_t command = kCommandAcquire;
|
||||
send_all(socket_fd_, &command, 1);
|
||||
read_status(socket_fd_);
|
||||
|
||||
const auto points = recv_u32(socket_fd_);
|
||||
if (points != settings_.sweep.points) {
|
||||
throw std::runtime_error("K209 remote server returned unexpected sweep point count");
|
||||
}
|
||||
const auto s11_values = recv_float32_array(socket_fd_, points * 2U, "S11");
|
||||
const auto s21_values = recv_float32_array(socket_fd_, points * 2U, "S21");
|
||||
|
||||
return SweepTrace{
|
||||
.frequency_hz = frequency_hz_,
|
||||
.s11 = complex_from_interleaved(s11_values, points, "S11"),
|
||||
.s21 = complex_from_interleaved(s21_values, points, "S21"),
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace radar::drivers
|
||||
-34
@@ -1,34 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "radar_driver.hpp"
|
||||
#include "run_config.hpp"
|
||||
|
||||
namespace radar::drivers {
|
||||
|
||||
struct RemoteCompactMK209DriverSettings {
|
||||
std::string host = "127.0.0.1";
|
||||
std::uint32_t port = 50209;
|
||||
config::RadarSweepSettings sweep{};
|
||||
std::uint32_t timeout_ms = 20'000;
|
||||
};
|
||||
|
||||
class RemoteCompactMK209Driver final : public RadarDriver {
|
||||
public:
|
||||
explicit RemoteCompactMK209Driver(RemoteCompactMK209DriverSettings settings);
|
||||
~RemoteCompactMK209Driver() override;
|
||||
|
||||
void open() override;
|
||||
void close() override;
|
||||
[[nodiscard]] auto acquire_sweep() -> SweepTrace override;
|
||||
|
||||
private:
|
||||
RemoteCompactMK209DriverSettings settings_{};
|
||||
int socket_fd_ = -1;
|
||||
std::vector<float> frequency_hz_{};
|
||||
};
|
||||
|
||||
} // namespace radar::drivers
|
||||
@@ -10,7 +10,6 @@
|
||||
#include "hmc349a_minimal_driver.hpp"
|
||||
#include "librevna_minimal_driver.hpp"
|
||||
#include "radar_driver.hpp"
|
||||
#include "remote_compact_m_k209_driver.hpp"
|
||||
#include "run_config.hpp"
|
||||
#include "shm_ring.hpp"
|
||||
#include "sweep_orchestrator.hpp"
|
||||
@@ -20,7 +19,6 @@ namespace {
|
||||
constexpr const char* kDefaultConfigPath = "run_config.json";
|
||||
constexpr const char* kLibreVnaModel = "librevna";
|
||||
constexpr const char* kLibreVnaMultiModel = "librevna_multi";
|
||||
constexpr const char* kCompactMK209Model = "compact_m_k209";
|
||||
std::atomic<bool> g_stop_requested{false};
|
||||
|
||||
void signal_handler(int /*signal*/) {
|
||||
@@ -48,26 +46,13 @@ void install_signal_handlers() {
|
||||
[[nodiscard]] auto make_radar_driver(const radar::config::RadarConfig& config)
|
||||
-> std::unique_ptr<radar::drivers::RadarDriver> {
|
||||
if (config.model == kLibreVnaModel || config.model.empty()) {
|
||||
if (config.driver_mode != radar::config::DriverMode::Native) {
|
||||
throw std::runtime_error("LibreVNA simulation uses python_app.scripts.matrix_raw_producer; launch through start.sh");
|
||||
}
|
||||
return std::make_unique<radar::drivers::LibreVnaMinimalDriver>(
|
||||
radar::drivers::LibreVnaMinimalDriverSettings{
|
||||
.mode = config.driver_mode,
|
||||
.serial = config.serial,
|
||||
.sweep = config.sweep,
|
||||
.mock_signal_hz = config.mock_signal_hz,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (config.model == kCompactMK209Model) {
|
||||
if (config.driver_mode != radar::config::DriverMode::Native) {
|
||||
throw std::runtime_error("compact_m_k209 requires radar.driver_mode='native'");
|
||||
}
|
||||
return std::make_unique<radar::drivers::RemoteCompactMK209Driver>(
|
||||
radar::drivers::RemoteCompactMK209DriverSettings{
|
||||
.host = config.remote_host,
|
||||
.port = config.remote_port,
|
||||
.sweep = config.sweep,
|
||||
.timeout_ms = 20'000,
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -76,10 +61,6 @@ void install_signal_handlers() {
|
||||
throw std::runtime_error("librevna_multi is handled by python_app.scripts.matrix_raw_producer");
|
||||
}
|
||||
|
||||
if (config.model == "sn9000") {
|
||||
throw std::runtime_error("sn9000 is handled by python_app.scripts.matrix_raw_producer");
|
||||
}
|
||||
|
||||
throw std::runtime_error("Unsupported radar.model for sweep_orchestrator: " + config.model);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
#include "sweep_orchestrator.hpp"
|
||||
#include "runtime_wait.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <exception>
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
namespace radar::acq {
|
||||
@@ -26,28 +25,6 @@ constexpr std::uint64_t kOpenRetryLogEvery = 30U;
|
||||
return stop_requested.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void sleep_if_needed_ms(std::uint32_t delay_ms) {
|
||||
if (delay_ms == 0U) {
|
||||
return;
|
||||
}
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(delay_ms));
|
||||
}
|
||||
|
||||
// Interruptible sleep: returns true if a stop was requested while waiting.
|
||||
[[nodiscard]] auto interruptible_sleep_ms(std::uint32_t delay_ms, const std::atomic<bool>& stop_requested) -> bool {
|
||||
constexpr std::uint32_t kPollMs = 50U;
|
||||
std::uint32_t waited = 0U;
|
||||
while (waited < delay_ms) {
|
||||
if (should_stop(stop_requested)) {
|
||||
return true;
|
||||
}
|
||||
const std::uint32_t chunk = std::min(kPollMs, delay_ms - waited);
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(chunk));
|
||||
waited += chunk;
|
||||
}
|
||||
return should_stop(stop_requested);
|
||||
}
|
||||
|
||||
void validate_sweep(const drivers::SweepTrace& sweep) {
|
||||
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");
|
||||
@@ -66,30 +43,14 @@ class DriverLifecycleGuard {
|
||||
output_switch_driver_(output_switch_driver) {}
|
||||
|
||||
~DriverLifecycleGuard() noexcept {
|
||||
if (!active_) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Shutdown should never hide the primary error path.
|
||||
try {
|
||||
output_switch_driver_.close();
|
||||
} catch (...) {
|
||||
}
|
||||
try {
|
||||
input_switch_driver_.close();
|
||||
} catch (...) {
|
||||
}
|
||||
try {
|
||||
radar_driver_.close();
|
||||
} catch (...) {
|
||||
}
|
||||
close_all();
|
||||
}
|
||||
|
||||
void open_all() {
|
||||
active_ = true; // A later open failure still owns partially opened devices.
|
||||
radar_driver_.open();
|
||||
input_switch_driver_.open();
|
||||
output_switch_driver_.open();
|
||||
active_ = true;
|
||||
}
|
||||
|
||||
// Open all devices, retrying forever with capped exponential backoff until success or
|
||||
@@ -118,7 +79,7 @@ class DriverLifecycleGuard {
|
||||
<< "); retrying up to every " << (kOpenRetryMaxMs / 1'000U)
|
||||
<< "s until present: " << exc.what() << '\n';
|
||||
}
|
||||
if (interruptible_sleep_ms(delay_ms, stop_requested)) {
|
||||
if (config::wait_or_stop(delay_ms, stop_requested)) {
|
||||
return false;
|
||||
}
|
||||
delay_ms = std::min(delay_ms * 2U, kOpenRetryMaxMs);
|
||||
@@ -127,14 +88,23 @@ class DriverLifecycleGuard {
|
||||
return false;
|
||||
}
|
||||
|
||||
void close_all() {
|
||||
void close_all() noexcept {
|
||||
if (!active_) {
|
||||
return;
|
||||
}
|
||||
output_switch_driver_.close();
|
||||
input_switch_driver_.close();
|
||||
radar_driver_.close();
|
||||
active_ = false;
|
||||
const auto close = [](auto& driver) {
|
||||
try {
|
||||
driver.close();
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << "sweep_orchestrator: driver cleanup failed: " << error.what() << '\n';
|
||||
} catch (...) {
|
||||
std::cerr << "sweep_orchestrator: driver cleanup failed with an unknown error\n";
|
||||
}
|
||||
};
|
||||
close(output_switch_driver_);
|
||||
close(input_switch_driver_);
|
||||
close(radar_driver_);
|
||||
}
|
||||
|
||||
private:
|
||||
@@ -238,7 +208,7 @@ void SweepOrchestrator::run(const std::atomic<bool>& stop_requested) {
|
||||
if (!config_.runtime.continuous) {
|
||||
break;
|
||||
}
|
||||
sleep_if_needed_ms(config_.runtime.idle_sleep_ms);
|
||||
(void)config::wait_or_stop(config_.runtime.idle_sleep_ms, stop_requested);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -289,11 +259,11 @@ auto SweepOrchestrator::acquire_one_collection(
|
||||
// 1) switch output path, 2) switch input path, 3) optional settle.
|
||||
output_switch_driver_.switch_to(combo.output_pos);
|
||||
input_switch_driver_.switch_to(combo.input_pos);
|
||||
sleep_if_needed_ms(config_.runtime.settling_ms);
|
||||
if (config::wait_or_stop(config_.runtime.settling_ms, stop_requested)) {
|
||||
interrupted = true;
|
||||
break;
|
||||
}
|
||||
|
||||
// Production drivers ignore this; mock drivers use it to give every
|
||||
// (input, output) pair its own synthetic response.
|
||||
radar_driver_.set_active_combo(combo);
|
||||
// Stamp around the sweep only: the switch drive and settling above belong to
|
||||
// neither the previous combo nor this one, so excluding them keeps the window
|
||||
// an honest "when was this combo actually measured".
|
||||
|
||||
Reference in New Issue
Block a user