init commit
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "shared_types.hpp"
|
||||
|
||||
namespace radar::drivers {
|
||||
|
||||
/**
|
||||
* @brief One S21 sweep acquired from the radar.
|
||||
*
|
||||
* Both vectors must have equal size and aligned indices.
|
||||
*/
|
||||
struct SweepTrace {
|
||||
std::vector<float> frequency_hz{};
|
||||
std::vector<ipc::Complex32> s21{};
|
||||
};
|
||||
|
||||
/**
|
||||
* @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.
|
||||
*/
|
||||
class RadarDriver {
|
||||
public:
|
||||
virtual ~RadarDriver() = default;
|
||||
|
||||
/** @brief Open underlying transport and prepare acquisition. */
|
||||
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;
|
||||
};
|
||||
|
||||
} // namespace radar::drivers
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace radar::drivers {
|
||||
|
||||
/**
|
||||
* @brief Unified RF switch interface consumed by the orchestrator.
|
||||
*/
|
||||
class SwitchDriver {
|
||||
public:
|
||||
virtual ~SwitchDriver() = default;
|
||||
|
||||
/** @brief Open underlying transport and set initial switch state. */
|
||||
virtual void open() = 0;
|
||||
/** @brief Release driver resources. */
|
||||
virtual void close() = 0;
|
||||
/** @brief Number of selectable positions exposed by this switch. */
|
||||
[[nodiscard]] virtual auto position_count() const -> std::uint32_t = 0;
|
||||
/** @brief Switch to a 0-based position. */
|
||||
virtual void switch_to(std::uint32_t position) = 0;
|
||||
};
|
||||
|
||||
} // namespace radar::drivers
|
||||
+212
@@ -0,0 +1,212 @@
|
||||
#include "../librevna_minimal_driver.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <exception>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <stdexcept>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "librevna_protocol_common.hpp"
|
||||
|
||||
namespace radar::drivers {
|
||||
namespace detail = radar::drivers::librevna::detail;
|
||||
namespace {
|
||||
|
||||
constexpr std::uint32_t kNativeAcquireMaxAttempts = 3U;
|
||||
|
||||
[[nodiscard]] auto is_retryable_native_acquire_error(std::string_view message) -> bool {
|
||||
constexpr std::array<std::string_view, 5> kRetryableSubstrings = {
|
||||
"Timeout waiting for expected LibreVNA packet type",
|
||||
"Timeout waiting for LibreVNA ACK",
|
||||
"LibreVNA returned NACK",
|
||||
"Failed to read LibreVNA USB bulk packet",
|
||||
"Failed to write LibreVNA USB bulk packet",
|
||||
};
|
||||
|
||||
return std::any_of(
|
||||
kRetryableSubstrings.begin(),
|
||||
kRetryableSubstrings.end(),
|
||||
[message](std::string_view needle) {
|
||||
return message.find(needle) != std::string_view::npos;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
LibreVnaMinimalDriver::LibreVnaMinimalDriver(LibreVnaMinimalDriverSettings settings) : settings_(std::move(settings)) {}
|
||||
|
||||
void LibreVnaMinimalDriver::open() {
|
||||
if (is_open_) {
|
||||
return;
|
||||
}
|
||||
if (settings_.sweep.points == 0U) {
|
||||
throw std::runtime_error("Radar sweep points must be > 0");
|
||||
}
|
||||
|
||||
if (settings_.mode == config::DriverMode::Native) {
|
||||
open_native();
|
||||
}
|
||||
|
||||
is_open_ = true;
|
||||
}
|
||||
|
||||
void LibreVnaMinimalDriver::close() {
|
||||
if (!is_open_) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (settings_.mode == config::DriverMode::Native) {
|
||||
close_native();
|
||||
}
|
||||
|
||||
is_open_ = false;
|
||||
}
|
||||
|
||||
auto LibreVnaMinimalDriver::acquire_s21_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{};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// Recover from transient USB/protocol stalls by reconnecting the device.
|
||||
close_native();
|
||||
open_native();
|
||||
}
|
||||
}
|
||||
|
||||
if (last_exception != nullptr) {
|
||||
std::rethrow_exception(last_exception);
|
||||
}
|
||||
throw std::runtime_error("Native acquisition failed without exception detail");
|
||||
}
|
||||
default:
|
||||
throw std::runtime_error("Unsupported radar driver mode");
|
||||
}
|
||||
}
|
||||
|
||||
auto LibreVnaMinimalDriver::acquire_mock() -> SweepTrace {
|
||||
SweepTrace trace{};
|
||||
trace.frequency_hz.reserve(settings_.sweep.points);
|
||||
trace.s21.reserve(settings_.sweep.points);
|
||||
|
||||
const auto span_hz = settings_.sweep.stop_hz - settings_.sweep.start_hz;
|
||||
const auto denominator = settings_.sweep.points > 1U ? static_cast<float>(settings_.sweep.points - 1U) : 1.0F;
|
||||
|
||||
for (std::uint32_t point = 0; point < settings_.sweep.points; ++point) {
|
||||
const auto ratio = static_cast<float>(point) / denominator;
|
||||
const auto frequency_hz = settings_.sweep.start_hz + span_hz * ratio;
|
||||
const auto phase = 2.0F * detail::kPi * (frequency_hz / std::max(settings_.mock_signal_hz, 1.0F)) +
|
||||
static_cast<float>(sweep_index_) * 0.05F;
|
||||
const auto envelope = 0.6F + 0.4F * std::sin(0.5F * phase);
|
||||
|
||||
ipc::Complex32 sample{};
|
||||
sample.re = envelope * std::cos(phase);
|
||||
sample.im = envelope * std::sin(phase);
|
||||
|
||||
trace.frequency_hz.push_back(frequency_hz);
|
||||
trace.s21.push_back(sample);
|
||||
}
|
||||
|
||||
return trace;
|
||||
}
|
||||
|
||||
auto LibreVnaMinimalDriver::acquire_native() -> SweepTrace {
|
||||
if (usb_handle_ == nullptr) {
|
||||
throw std::runtime_error("LibreVNA native handle is not open");
|
||||
}
|
||||
|
||||
// Drop stale datapoints left in queue by previous operations.
|
||||
while (pop_packet(detail::kPacketVnaDatapoint).has_value()) {
|
||||
}
|
||||
|
||||
send_packet_no_payload(detail::kPacketInitiateSweep, true);
|
||||
|
||||
SweepTrace trace{};
|
||||
trace.frequency_hz.assign(settings_.sweep.points, 0.0F);
|
||||
trace.s21.assign(settings_.sweep.points, ipc::Complex32{});
|
||||
|
||||
std::vector<std::uint8_t> received(settings_.sweep.points, 0U);
|
||||
std::uint32_t received_count = 0;
|
||||
|
||||
const auto ifbw_hz = std::max(settings_.sweep.if_bandwidth_hz, 1.0F);
|
||||
const auto estimated_sweep_ms = static_cast<std::uint64_t>(
|
||||
std::ceil((1'000.0 * static_cast<double>(settings_.sweep.points)) / static_cast<double>(ifbw_hz))
|
||||
);
|
||||
// Keep generous timeout margin on slower hosts.
|
||||
const auto timeout_ms = std::max<std::uint64_t>(20'000ULL, estimated_sweep_ms * 8ULL + 1'000ULL);
|
||||
const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(timeout_ms);
|
||||
|
||||
while (received_count < settings_.sweep.points) {
|
||||
NativePacket packet{};
|
||||
try {
|
||||
packet = wait_for_packet(detail::kPacketVnaDatapoint, deadline);
|
||||
} catch (const std::exception& exception) {
|
||||
throw std::runtime_error(
|
||||
"Timeout while collecting VNADatapoints (" + std::to_string(received_count) + "/" +
|
||||
std::to_string(settings_.sweep.points) + " points received): " + exception.what()
|
||||
);
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
if (point_number >= settings_.sweep.points) {
|
||||
throw std::runtime_error("Received out-of-range VNADatapoint index");
|
||||
}
|
||||
|
||||
if (received[point_number] == 0U) {
|
||||
received[point_number] = 1U;
|
||||
++received_count;
|
||||
}
|
||||
|
||||
trace.frequency_hz[point_number] = frequency_hz;
|
||||
trace.s21[point_number] = s21;
|
||||
}
|
||||
|
||||
return trace;
|
||||
}
|
||||
|
||||
void LibreVnaMinimalDriver::validate_device_info_payload(std::span<const std::uint8_t> payload) const {
|
||||
if (payload.size() != detail::kDeviceInfoPayloadSize) {
|
||||
throw std::runtime_error("Unexpected DeviceInfo payload size");
|
||||
}
|
||||
|
||||
const auto protocol = detail::read_u16_le(payload, detail::kDeviceInfoProtocolOffset);
|
||||
if (protocol != detail::kProtocolV14) {
|
||||
throw std::runtime_error("Unsupported LibreVNA protocol version (expected v14)");
|
||||
}
|
||||
|
||||
const auto ports = payload[detail::kDeviceInfoNumPortsOffset];
|
||||
if (ports < 2U) {
|
||||
throw std::runtime_error("LibreVNA reported invalid number of ports");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace radar::drivers
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
#include "../librevna_minimal_driver.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <complex>
|
||||
#include <cstddef>
|
||||
#include <stdexcept>
|
||||
#include <vector>
|
||||
|
||||
#include "librevna_protocol_common.hpp"
|
||||
|
||||
namespace radar::drivers {
|
||||
namespace detail = radar::drivers::librevna::detail;
|
||||
|
||||
auto LibreVnaMinimalDriver::encode_frame(
|
||||
std::uint8_t packet_type,
|
||||
std::span<const std::uint8_t> payload
|
||||
) -> std::vector<std::uint8_t> {
|
||||
const auto length = payload.size() + detail::kFrameOverheadBytes;
|
||||
if (length > 0xFFFFU) {
|
||||
throw std::runtime_error("Protocol frame is too large");
|
||||
}
|
||||
|
||||
std::vector<std::uint8_t> frame(length, 0);
|
||||
frame[0] = detail::kFrameHeader;
|
||||
detail::write_u16_le(frame, 1, static_cast<std::uint16_t>(length));
|
||||
frame[3] = packet_type;
|
||||
std::copy(payload.begin(), payload.end(), frame.begin() + 4);
|
||||
|
||||
std::uint32_t crc = 0;
|
||||
if (packet_type != detail::kPacketVnaDatapoint) {
|
||||
crc = crc32(std::span<const std::uint8_t>(frame.data(), frame.size() - 4U));
|
||||
}
|
||||
detail::write_u32_le(frame, frame.size() - 4U, crc);
|
||||
|
||||
return frame;
|
||||
}
|
||||
|
||||
auto LibreVnaMinimalDriver::decode_vna_datapoint_s21(
|
||||
std::span<const std::uint8_t> payload,
|
||||
std::uint32_t& point_number_out,
|
||||
float& frequency_out,
|
||||
ipc::Complex32& s21_out
|
||||
) -> bool {
|
||||
// VNADatapoint payload layout:
|
||||
// [0..7]=freq_or_time, [8..9]=cdbm, [10..11]=point_number,
|
||||
// followed by N tuples of {real:f32, imag:f32, flags:u8}.
|
||||
if (payload.size() < 12U) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto values_block = payload.size() - 12U;
|
||||
if ((values_block % 9U) != 0U) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto num_values = values_block / 9U;
|
||||
if (num_values == 0U) {
|
||||
return false;
|
||||
}
|
||||
|
||||
point_number_out = detail::read_u16_le(payload, 10);
|
||||
frequency_out = 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<bool, 8> has_ref{};
|
||||
std::array<bool, 8> has_measured{};
|
||||
|
||||
for (std::size_t index = 0; index < num_values; ++index) {
|
||||
const auto flags = payload[flags_offset + index];
|
||||
const auto stage = static_cast<std::size_t>(flags >> 5U);
|
||||
if (stage >= ref_by_stage.size()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto value = std::complex<float>(
|
||||
detail::read_f32_le(payload, real_offset + (4U * index)),
|
||||
detail::read_f32_le(payload, imag_offset + (4U * index))
|
||||
);
|
||||
|
||||
const bool is_reference = (flags & detail::kReferenceFlagMask) != 0U;
|
||||
if ((flags & detail::kPort1Mask) != 0U && is_reference) {
|
||||
ref_by_stage[stage] = value;
|
||||
has_ref[stage] = true;
|
||||
}
|
||||
if ((flags & detail::kPort2Mask) != 0U && !is_reference) {
|
||||
measured_by_stage[stage] = value;
|
||||
has_measured[stage] = true;
|
||||
}
|
||||
}
|
||||
|
||||
// We need one reference sample from port1 and one measured sample from port2.
|
||||
for (std::size_t stage = 0; stage < ref_by_stage.size(); ++stage) {
|
||||
if (!has_ref[stage] || !has_measured[stage]) {
|
||||
continue;
|
||||
}
|
||||
if (std::norm(ref_by_stage[stage]) <= 0.0F) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto ratio = measured_by_stage[stage] / ref_by_stage[stage];
|
||||
s21_out.re = ratio.real();
|
||||
s21_out.im = ratio.imag();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
auto LibreVnaMinimalDriver::crc32(std::span<const std::uint8_t> data) -> std::uint32_t {
|
||||
std::uint32_t crc = 0xFFFFFFFFU;
|
||||
for (const auto byte : data) {
|
||||
crc ^= static_cast<std::uint32_t>(byte);
|
||||
for (int bit = 0; bit < 8; ++bit) {
|
||||
const auto lsb = crc & 1U;
|
||||
crc >>= 1U;
|
||||
if (lsb != 0U) {
|
||||
crc ^= 0xEDB88320U;
|
||||
}
|
||||
}
|
||||
}
|
||||
return ~crc;
|
||||
}
|
||||
|
||||
} // namespace radar::drivers
|
||||
|
||||
+359
@@ -0,0 +1,359 @@
|
||||
#include "../librevna_minimal_driver.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cerrno>
|
||||
#include <cstring>
|
||||
#include <fcntl.h>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "librevna_protocol_common.hpp"
|
||||
|
||||
namespace radar::drivers {
|
||||
namespace detail = radar::drivers::librevna::detail;
|
||||
namespace {
|
||||
|
||||
[[nodiscard]] auto libusb_error_message(const std::string& prefix, int status_code) -> std::string {
|
||||
return prefix + ": " + libusb_error_name(status_code);
|
||||
}
|
||||
|
||||
[[nodiscard]] auto is_supported_vid_pid(std::uint16_t vendor_id, std::uint16_t product_id) -> bool {
|
||||
return std::any_of(
|
||||
detail::kSupportedUsbIds.begin(),
|
||||
detail::kSupportedUsbIds.end(),
|
||||
[vendor_id, product_id](const auto& pair) {
|
||||
return pair.first == vendor_id && pair.second == product_id;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
[[nodiscard]] auto parse_serial_string(libusb_device_handle* handle, std::uint8_t serial_index) -> std::string {
|
||||
if (handle == nullptr || serial_index == 0U) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::array<unsigned char, 256> serial_bytes{};
|
||||
const auto length = libusb_get_string_descriptor_ascii(
|
||||
handle,
|
||||
serial_index,
|
||||
serial_bytes.data(),
|
||||
static_cast<int>(serial_bytes.size())
|
||||
);
|
||||
if (length <= 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return std::string(
|
||||
reinterpret_cast<const char*>(serial_bytes.data()),
|
||||
reinterpret_cast<const char*>(serial_bytes.data() + length)
|
||||
);
|
||||
}
|
||||
|
||||
[[nodiscard]] auto find_matching_device_handle(
|
||||
libusb_context* usb_context,
|
||||
const std::string& expected_serial
|
||||
) -> libusb_device_handle* {
|
||||
libusb_device** devices = nullptr;
|
||||
const auto device_count = libusb_get_device_list(usb_context, &devices);
|
||||
if (device_count < 0) {
|
||||
throw std::runtime_error(
|
||||
libusb_error_message("Failed to enumerate USB devices", static_cast<int>(device_count))
|
||||
);
|
||||
}
|
||||
|
||||
libusb_device_handle* selected_handle = nullptr;
|
||||
for (ssize_t index = 0; index < device_count; ++index) {
|
||||
auto* device = devices[index];
|
||||
if (device == nullptr) {
|
||||
continue;
|
||||
}
|
||||
|
||||
libusb_device_descriptor descriptor{};
|
||||
const auto descriptor_status = libusb_get_device_descriptor(device, &descriptor);
|
||||
if (descriptor_status != LIBUSB_SUCCESS) {
|
||||
continue;
|
||||
}
|
||||
if (!is_supported_vid_pid(descriptor.idVendor, descriptor.idProduct)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
libusb_device_handle* candidate_handle = nullptr;
|
||||
if (libusb_open(device, &candidate_handle) != LIBUSB_SUCCESS || candidate_handle == nullptr) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!expected_serial.empty()) {
|
||||
const auto serial = parse_serial_string(candidate_handle, descriptor.iSerialNumber);
|
||||
if (serial != expected_serial) {
|
||||
libusb_close(candidate_handle);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
selected_handle = candidate_handle;
|
||||
break;
|
||||
}
|
||||
|
||||
libusb_free_device_list(devices, 1);
|
||||
return selected_handle;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void LibreVnaMinimalDriver::open_native() {
|
||||
if (usb_context_ != nullptr || usb_handle_ != nullptr) {
|
||||
throw std::runtime_error("LibreVNA native state is already initialized");
|
||||
}
|
||||
|
||||
const auto init_status = libusb_init(&usb_context_);
|
||||
if (init_status != LIBUSB_SUCCESS) {
|
||||
usb_context_ = nullptr;
|
||||
throw std::runtime_error(libusb_error_message("Failed to initialize libusb", init_status));
|
||||
}
|
||||
|
||||
try {
|
||||
auto* selected_handle = find_matching_device_handle(usb_context_, settings_.serial);
|
||||
|
||||
if (selected_handle == nullptr) {
|
||||
const auto serial_hint = settings_.serial.empty() ? std::string() : " for serial '" + settings_.serial + "'";
|
||||
throw std::runtime_error("No compatible LibreVNA USB device found" + serial_hint);
|
||||
}
|
||||
|
||||
usb_handle_ = selected_handle;
|
||||
|
||||
const auto auto_detach_status = libusb_set_auto_detach_kernel_driver(usb_handle_, 1);
|
||||
if (auto_detach_status != LIBUSB_SUCCESS && auto_detach_status != LIBUSB_ERROR_NOT_SUPPORTED) {
|
||||
throw std::runtime_error(
|
||||
libusb_error_message("Failed to configure USB auto-detach kernel driver", auto_detach_status)
|
||||
);
|
||||
}
|
||||
|
||||
const auto claim_status = libusb_claim_interface(usb_handle_, detail::kUsbInterface);
|
||||
if (claim_status != LIBUSB_SUCCESS) {
|
||||
throw std::runtime_error(libusb_error_message("Failed to claim LibreVNA USB interface", claim_status));
|
||||
}
|
||||
interface_claimed_ = true;
|
||||
|
||||
rx_buffer_.clear();
|
||||
packet_queue_.clear();
|
||||
|
||||
send_packet_no_payload(detail::kPacketRequestDeviceInfo, true);
|
||||
const auto info_packet = wait_for_packet(
|
||||
detail::kPacketDeviceInfo,
|
||||
std::chrono::steady_clock::now() + std::chrono::seconds(detail::kDeviceInfoTimeoutSeconds)
|
||||
);
|
||||
validate_device_info_payload(info_packet.payload);
|
||||
protocol_version_ = detail::read_u16_le(info_packet.payload, detail::kDeviceInfoProtocolOffset);
|
||||
device_num_ports_ = info_packet.payload[detail::kDeviceInfoNumPortsOffset];
|
||||
} catch (...) {
|
||||
close_native();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
void LibreVnaMinimalDriver::close_native() {
|
||||
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;
|
||||
}
|
||||
|
||||
if (usb_context_ != nullptr) {
|
||||
libusb_exit(usb_context_);
|
||||
usb_context_ = nullptr;
|
||||
}
|
||||
|
||||
protocol_version_ = 0;
|
||||
device_num_ports_ = 0;
|
||||
}
|
||||
|
||||
void LibreVnaMinimalDriver::send_packet(
|
||||
std::uint8_t packet_type,
|
||||
std::span<const std::uint8_t> payload,
|
||||
bool require_ack
|
||||
) {
|
||||
if (usb_handle_ == nullptr) {
|
||||
throw std::runtime_error("Cannot send packet: LibreVNA USB handle is not open");
|
||||
}
|
||||
|
||||
auto frame = encode_frame(packet_type, payload);
|
||||
|
||||
int transferred = 0;
|
||||
const auto transfer_status = libusb_bulk_transfer(
|
||||
usb_handle_,
|
||||
detail::kEndpointOut,
|
||||
reinterpret_cast<unsigned char*>(frame.data()),
|
||||
static_cast<int>(frame.size()),
|
||||
&transferred,
|
||||
detail::kUsbWriteTimeoutMs
|
||||
);
|
||||
if (transfer_status != LIBUSB_SUCCESS) {
|
||||
throw std::runtime_error(libusb_error_message("Failed to write LibreVNA USB bulk packet", transfer_status));
|
||||
}
|
||||
if (transferred != static_cast<int>(frame.size())) {
|
||||
throw std::runtime_error("Incomplete LibreVNA USB bulk write");
|
||||
}
|
||||
|
||||
if (require_ack) {
|
||||
wait_for_ack(std::chrono::steady_clock::now() + std::chrono::milliseconds(detail::kAckTimeoutMs));
|
||||
}
|
||||
}
|
||||
|
||||
void LibreVnaMinimalDriver::send_packet_no_payload(std::uint8_t packet_type, bool require_ack) {
|
||||
send_packet(packet_type, std::span<const std::uint8_t>{}, require_ack);
|
||||
}
|
||||
|
||||
void LibreVnaMinimalDriver::wait_for_ack(std::chrono::steady_clock::time_point deadline) {
|
||||
while (std::chrono::steady_clock::now() < deadline) {
|
||||
std::optional<NativePacket> ack_or_nack{};
|
||||
for (auto iter = packet_queue_.begin(); iter != packet_queue_.end(); ++iter) {
|
||||
if (iter->packet_type != detail::kPacketAck && iter->packet_type != detail::kPacketNack) {
|
||||
continue;
|
||||
}
|
||||
ack_or_nack = std::move(*iter);
|
||||
packet_queue_.erase(iter);
|
||||
break;
|
||||
}
|
||||
|
||||
if (ack_or_nack.has_value()) {
|
||||
if (ack_or_nack->packet_type == detail::kPacketAck) {
|
||||
return;
|
||||
}
|
||||
throw std::runtime_error("LibreVNA returned NACK");
|
||||
}
|
||||
|
||||
pump_usb(deadline);
|
||||
}
|
||||
|
||||
throw std::runtime_error("Timeout waiting for LibreVNA ACK");
|
||||
}
|
||||
|
||||
auto LibreVnaMinimalDriver::wait_for_packet(
|
||||
std::uint8_t expected_type,
|
||||
std::chrono::steady_clock::time_point deadline
|
||||
) -> NativePacket {
|
||||
while (std::chrono::steady_clock::now() < deadline) {
|
||||
if (auto packet = pop_packet(expected_type); packet.has_value()) {
|
||||
return *packet;
|
||||
}
|
||||
pump_usb(deadline);
|
||||
}
|
||||
|
||||
throw std::runtime_error("Timeout waiting for expected LibreVNA packet type");
|
||||
}
|
||||
|
||||
void LibreVnaMinimalDriver::pump_usb(std::chrono::steady_clock::time_point deadline) {
|
||||
if (usb_handle_ == nullptr) {
|
||||
throw std::runtime_error("Cannot read packets: LibreVNA USB handle is not open");
|
||||
}
|
||||
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
if (now >= deadline) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto remaining_ms = std::chrono::duration_cast<std::chrono::milliseconds>(deadline - now).count();
|
||||
const auto timeout_ms = static_cast<unsigned int>(
|
||||
std::clamp<std::int64_t>(remaining_ms, detail::kUsbReadPollMinTimeoutMs, detail::kUsbReadPollMaxTimeoutMs)
|
||||
);
|
||||
|
||||
std::array<unsigned char, detail::kUsbReadChunkBytes> buffer{};
|
||||
int transferred = 0;
|
||||
const auto transfer_status = libusb_bulk_transfer(
|
||||
usb_handle_,
|
||||
detail::kEndpointIn,
|
||||
buffer.data(),
|
||||
static_cast<int>(buffer.size()),
|
||||
&transferred,
|
||||
timeout_ms
|
||||
);
|
||||
|
||||
if (transfer_status == LIBUSB_ERROR_TIMEOUT) {
|
||||
return;
|
||||
}
|
||||
if (transfer_status != LIBUSB_SUCCESS) {
|
||||
throw std::runtime_error(libusb_error_message("Failed to read LibreVNA USB bulk packet", transfer_status));
|
||||
}
|
||||
if (transferred <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
rx_buffer_.insert(rx_buffer_.end(), buffer.begin(), buffer.begin() + transferred);
|
||||
decode_frames_from_buffer();
|
||||
}
|
||||
|
||||
void LibreVnaMinimalDriver::decode_frames_from_buffer() {
|
||||
while (true) {
|
||||
const auto header_iter = std::find(rx_buffer_.begin(), rx_buffer_.end(), detail::kFrameHeader);
|
||||
if (header_iter == rx_buffer_.end()) {
|
||||
rx_buffer_.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
if (header_iter != rx_buffer_.begin()) {
|
||||
rx_buffer_.erase(rx_buffer_.begin(), header_iter);
|
||||
}
|
||||
|
||||
if (rx_buffer_.size() < 4U) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto declared_length = detail::read_u16_le(rx_buffer_, 1);
|
||||
if (declared_length < detail::kFrameOverheadBytes || declared_length > detail::kMaxFrameLengthBytes) {
|
||||
rx_buffer_.erase(rx_buffer_.begin());
|
||||
continue;
|
||||
}
|
||||
|
||||
if (rx_buffer_.size() < declared_length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const std::vector<std::uint8_t> frame(rx_buffer_.begin(), rx_buffer_.begin() + declared_length);
|
||||
rx_buffer_.erase(rx_buffer_.begin(), rx_buffer_.begin() + declared_length);
|
||||
|
||||
const auto packet_type = frame[3];
|
||||
const auto received_crc = detail::read_u32_le(frame, frame.size() - 4U);
|
||||
|
||||
if (packet_type == detail::kPacketVnaDatapoint) {
|
||||
if (received_crc != 0U) {
|
||||
throw std::runtime_error("Invalid VNADatapoint CRC (expected zero)");
|
||||
}
|
||||
} else {
|
||||
const auto expected_crc = crc32(std::span<const std::uint8_t>(frame.data(), frame.size() - 4U));
|
||||
if (received_crc != expected_crc) {
|
||||
throw std::runtime_error("Invalid LibreVNA packet CRC");
|
||||
}
|
||||
}
|
||||
|
||||
NativePacket packet{};
|
||||
packet.packet_type = packet_type;
|
||||
packet.payload.assign(frame.begin() + 4, frame.end() - 4);
|
||||
packet_queue_.push_back(std::move(packet));
|
||||
}
|
||||
}
|
||||
|
||||
auto LibreVnaMinimalDriver::pop_packet(std::uint8_t packet_type) -> std::optional<NativePacket> {
|
||||
for (auto iter = packet_queue_.begin(); iter != packet_queue_.end(); ++iter) {
|
||||
if (iter->packet_type != packet_type) {
|
||||
continue;
|
||||
}
|
||||
|
||||
auto packet = std::move(*iter);
|
||||
packet_queue_.erase(iter);
|
||||
return packet;
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
} // namespace radar::drivers
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <span>
|
||||
#include <stdexcept>
|
||||
#include <utility>
|
||||
|
||||
namespace radar::drivers::librevna::detail {
|
||||
|
||||
constexpr float kPi = 3.14159265358979323846F;
|
||||
|
||||
// Frame format:
|
||||
// [0]=header, [1..2]=length_le, [3]=packet_type, [...payload...], [crc32_le]
|
||||
constexpr std::uint8_t kFrameHeader = 0x5A;
|
||||
constexpr std::size_t kFrameOverheadBytes = 8;
|
||||
constexpr std::size_t kMaxFrameLengthBytes = 4096;
|
||||
|
||||
constexpr unsigned char kEndpointOut = 0x01;
|
||||
constexpr unsigned char kEndpointIn = 0x81;
|
||||
constexpr int kUsbInterface = 0;
|
||||
|
||||
constexpr std::uint8_t kPacketDeviceInfo = 5;
|
||||
constexpr std::uint8_t kPacketAck = 7;
|
||||
constexpr std::uint8_t kPacketNack = 10;
|
||||
constexpr std::uint8_t kPacketRequestDeviceInfo = 15;
|
||||
constexpr std::uint8_t kPacketVnaDatapoint = 27;
|
||||
constexpr std::uint8_t kPacketInitiateSweep = 32;
|
||||
|
||||
constexpr std::size_t kDeviceInfoPayloadSize = 57;
|
||||
constexpr std::size_t kDeviceInfoProtocolOffset = 0;
|
||||
constexpr std::size_t kDeviceInfoNumPortsOffset = 54;
|
||||
constexpr std::uint16_t kProtocolV14 = 14;
|
||||
|
||||
constexpr std::uint8_t kReferenceFlagMask = 0x10;
|
||||
constexpr std::uint8_t kPort1Mask = 0x01;
|
||||
constexpr std::uint8_t kPort2Mask = 0x02;
|
||||
|
||||
constexpr std::array<std::pair<std::uint16_t, std::uint16_t>, 3> kSupportedUsbIds = {
|
||||
std::pair<std::uint16_t, std::uint16_t>{0x0483, 0x564E},
|
||||
std::pair<std::uint16_t, std::uint16_t>{0x0483, 0x4121},
|
||||
std::pair<std::uint16_t, std::uint16_t>{0x1209, 0x4121},
|
||||
};
|
||||
|
||||
constexpr int kUsbWriteTimeoutMs = 500;
|
||||
constexpr int kAckTimeoutMs = 800;
|
||||
constexpr int kDeviceInfoTimeoutSeconds = 2;
|
||||
constexpr std::size_t kUsbReadChunkBytes = 16 * 1024;
|
||||
constexpr int kUsbReadPollMinTimeoutMs = 1;
|
||||
constexpr int kUsbReadPollMaxTimeoutMs = 50;
|
||||
|
||||
[[nodiscard]] inline auto read_u16_le(std::span<const std::uint8_t> data, std::size_t offset) -> std::uint16_t {
|
||||
if ((offset + sizeof(std::uint16_t)) > data.size()) {
|
||||
throw std::runtime_error("Failed to decode uint16 from payload");
|
||||
}
|
||||
return static_cast<std::uint16_t>(data[offset]) |
|
||||
(static_cast<std::uint16_t>(data[offset + 1]) << 8U);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline auto read_u32_le(std::span<const std::uint8_t> data, std::size_t offset) -> std::uint32_t {
|
||||
if ((offset + sizeof(std::uint32_t)) > data.size()) {
|
||||
throw std::runtime_error("Failed to decode uint32 from payload");
|
||||
}
|
||||
return static_cast<std::uint32_t>(data[offset]) |
|
||||
(static_cast<std::uint32_t>(data[offset + 1]) << 8U) |
|
||||
(static_cast<std::uint32_t>(data[offset + 2]) << 16U) |
|
||||
(static_cast<std::uint32_t>(data[offset + 3]) << 24U);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline auto read_u64_le(std::span<const std::uint8_t> data, std::size_t offset) -> std::uint64_t {
|
||||
if ((offset + sizeof(std::uint64_t)) > data.size()) {
|
||||
throw std::runtime_error("Failed to decode uint64 from payload");
|
||||
}
|
||||
|
||||
std::uint64_t value = 0;
|
||||
for (std::size_t index = 0; index < sizeof(std::uint64_t); ++index) {
|
||||
value |= static_cast<std::uint64_t>(data[offset + index]) << (index * 8U);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
[[nodiscard]] inline auto read_f32_le(std::span<const std::uint8_t> data, std::size_t offset) -> float {
|
||||
const auto raw = read_u32_le(data, offset);
|
||||
float out = 0.0F;
|
||||
std::memcpy(&out, &raw, sizeof(float));
|
||||
return out;
|
||||
}
|
||||
|
||||
inline void write_u16_le(std::span<std::uint8_t> data, std::size_t offset, std::uint16_t value) {
|
||||
if ((offset + sizeof(std::uint16_t)) > data.size()) {
|
||||
throw std::runtime_error("Failed to encode uint16 into frame");
|
||||
}
|
||||
|
||||
data[offset] = static_cast<std::uint8_t>(value & 0xFFU);
|
||||
data[offset + 1] = static_cast<std::uint8_t>((value >> 8U) & 0xFFU);
|
||||
}
|
||||
|
||||
inline void write_u32_le(std::span<std::uint8_t> data, std::size_t offset, std::uint32_t value) {
|
||||
if ((offset + sizeof(std::uint32_t)) > data.size()) {
|
||||
throw std::runtime_error("Failed to encode uint32 into frame");
|
||||
}
|
||||
|
||||
data[offset] = static_cast<std::uint8_t>(value & 0xFFU);
|
||||
data[offset + 1] = static_cast<std::uint8_t>((value >> 8U) & 0xFFU);
|
||||
data[offset + 2] = static_cast<std::uint8_t>((value >> 16U) & 0xFFU);
|
||||
data[offset + 3] = static_cast<std::uint8_t>((value >> 24U) & 0xFFU);
|
||||
}
|
||||
|
||||
} // namespace radar::drivers::librevna::detail
|
||||
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <deque>
|
||||
#include <optional>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#if __has_include(<libusb-1.0/libusb.h>)
|
||||
#include <libusb-1.0/libusb.h>
|
||||
#else
|
||||
#include <libusb.h>
|
||||
#endif
|
||||
|
||||
#include "radar_driver.hpp"
|
||||
#include "run_config.hpp"
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Minimal radar driver that acquires S21 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
|
||||
* Python layer before this process starts.
|
||||
*/
|
||||
class LibreVnaMinimalDriver final : public RadarDriver {
|
||||
public:
|
||||
explicit LibreVnaMinimalDriver(LibreVnaMinimalDriverSettings settings);
|
||||
|
||||
void open() override;
|
||||
void close() override;
|
||||
[[nodiscard]] auto acquire_s21_sweep() -> SweepTrace override;
|
||||
|
||||
private:
|
||||
/**
|
||||
* @brief Raw protocol packet decoded from framed USB stream.
|
||||
*/
|
||||
struct NativePacket {
|
||||
std::uint8_t packet_type = 0;
|
||||
std::vector<std::uint8_t> payload{};
|
||||
};
|
||||
|
||||
[[nodiscard]] auto acquire_mock() -> SweepTrace;
|
||||
[[nodiscard]] auto acquire_native() -> SweepTrace;
|
||||
|
||||
void open_native();
|
||||
void close_native();
|
||||
|
||||
void send_packet(std::uint8_t packet_type, std::span<const std::uint8_t> payload, bool require_ack);
|
||||
void send_packet_no_payload(std::uint8_t packet_type, bool require_ack);
|
||||
void wait_for_ack(std::chrono::steady_clock::time_point deadline);
|
||||
[[nodiscard]] auto wait_for_packet(std::uint8_t expected_type, std::chrono::steady_clock::time_point deadline)
|
||||
-> NativePacket;
|
||||
void pump_usb(std::chrono::steady_clock::time_point deadline);
|
||||
void decode_frames_from_buffer();
|
||||
|
||||
[[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(
|
||||
std::span<const std::uint8_t> payload,
|
||||
std::uint32_t& point_number_out,
|
||||
float& frequency_out,
|
||||
ipc::Complex32& s21_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;
|
||||
|
||||
LibreVnaMinimalDriverSettings settings_{};
|
||||
bool is_open_ = false;
|
||||
std::uint64_t sweep_index_ = 0;
|
||||
|
||||
libusb_context* usb_context_ = nullptr;
|
||||
libusb_device_handle* usb_handle_ = nullptr;
|
||||
bool interface_claimed_ = false;
|
||||
|
||||
std::vector<std::uint8_t> rx_buffer_{};
|
||||
std::deque<NativePacket> packet_queue_{};
|
||||
|
||||
std::uint16_t protocol_version_ = 0;
|
||||
std::uint8_t device_num_ports_ = 0;
|
||||
};
|
||||
|
||||
} // namespace radar::drivers
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
#include "h7992_minimal_driver.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <cerrno>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <fcntl.h>
|
||||
#include <linux/gpio.h>
|
||||
#include <stdexcept>
|
||||
#include <sys/ioctl.h>
|
||||
#include <unistd.h>
|
||||
#include <utility>
|
||||
|
||||
namespace radar::drivers {
|
||||
namespace {
|
||||
|
||||
// Position mapping for H7992 control pins:
|
||||
// position -> (A, B)
|
||||
constexpr std::array<std::array<std::uint8_t, 2>, 4> kPositionToAB = {
|
||||
std::array<std::uint8_t, 2>{0, 0},
|
||||
std::array<std::uint8_t, 2>{0, 1},
|
||||
std::array<std::uint8_t, 2>{1, 0},
|
||||
std::array<std::uint8_t, 2>{1, 1},
|
||||
};
|
||||
|
||||
void validate_open_settings(const H7992MinimalDriverSettings& settings) {
|
||||
if (settings.positions == 0U || settings.positions > 4U) {
|
||||
throw std::runtime_error("H7992 switch positions must be in range [1, 4] for " + settings.name);
|
||||
}
|
||||
if (settings.default_position >= settings.positions) {
|
||||
throw std::runtime_error("Switch default_position out of range for " + settings.name);
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] auto make_line_values(std::uint8_t pin_a_state, std::uint8_t pin_b_state) -> gpio_v2_line_values {
|
||||
gpio_v2_line_values values{};
|
||||
values.mask = (1ULL << 0U) | (1ULL << 1U);
|
||||
values.bits =
|
||||
(static_cast<std::uint64_t>(pin_a_state) << 0U) | (static_cast<std::uint64_t>(pin_b_state) << 1U);
|
||||
return values;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
H7992MinimalDriver::H7992MinimalDriver(H7992MinimalDriverSettings settings) : settings_(std::move(settings)) {}
|
||||
|
||||
void H7992MinimalDriver::open() {
|
||||
if (is_open_) {
|
||||
return;
|
||||
}
|
||||
validate_open_settings(settings_);
|
||||
|
||||
if (settings_.mode == config::DriverMode::Native) {
|
||||
open_native();
|
||||
}
|
||||
|
||||
is_open_ = true;
|
||||
switch_to(settings_.default_position);
|
||||
}
|
||||
|
||||
void H7992MinimalDriver::close() {
|
||||
if (!is_open_) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (settings_.mode == config::DriverMode::Native) {
|
||||
close_native();
|
||||
}
|
||||
|
||||
is_open_ = false;
|
||||
}
|
||||
|
||||
auto H7992MinimalDriver::position_count() const -> std::uint32_t {
|
||||
return settings_.positions;
|
||||
}
|
||||
|
||||
void H7992MinimalDriver::switch_to(std::uint32_t position) {
|
||||
if (!is_open_) {
|
||||
throw std::runtime_error("Switch driver is not open for " + settings_.name);
|
||||
}
|
||||
if (position >= settings_.positions) {
|
||||
throw std::runtime_error("Switch position out of range for " + settings_.name);
|
||||
}
|
||||
|
||||
switch (settings_.mode) {
|
||||
case config::DriverMode::Mock:
|
||||
break;
|
||||
case config::DriverMode::Native:
|
||||
switch_native(position);
|
||||
break;
|
||||
default:
|
||||
throw std::runtime_error("Unsupported switch driver mode for " + settings_.name);
|
||||
}
|
||||
|
||||
current_position_ = position;
|
||||
}
|
||||
|
||||
void H7992MinimalDriver::open_native() {
|
||||
if (settings_.gpio_chip.empty()) {
|
||||
throw std::runtime_error("gpio_chip is empty for native switch " + settings_.name);
|
||||
}
|
||||
if (settings_.pin_a < 0 || settings_.pin_b < 0 || settings_.pin_a == settings_.pin_b) {
|
||||
throw std::runtime_error("Invalid pin_a/pin_b for native switch " + settings_.name);
|
||||
}
|
||||
|
||||
chip_fd_ = ::open(settings_.gpio_chip.c_str(), O_RDONLY | O_CLOEXEC);
|
||||
if (chip_fd_ < 0) {
|
||||
throw std::runtime_error(
|
||||
"Failed to open gpio chip '" + settings_.gpio_chip + "' for " + settings_.name + ": " + std::strerror(errno)
|
||||
);
|
||||
}
|
||||
|
||||
gpio_v2_line_request request{};
|
||||
// The first requested line maps to logical bit 0, the second to bit 1.
|
||||
request.offsets[0] = static_cast<std::uint32_t>(settings_.pin_a);
|
||||
request.offsets[1] = static_cast<std::uint32_t>(settings_.pin_b);
|
||||
request.num_lines = 2;
|
||||
request.config.flags = GPIO_V2_LINE_FLAG_OUTPUT;
|
||||
std::snprintf(request.consumer, sizeof(request.consumer), "radar_%s", settings_.name.c_str());
|
||||
|
||||
if (::ioctl(chip_fd_, GPIO_V2_GET_LINE_IOCTL, &request) != 0) {
|
||||
const auto error = std::strerror(errno);
|
||||
::close(chip_fd_);
|
||||
chip_fd_ = -1;
|
||||
throw std::runtime_error("Failed to request GPIO lines for " + settings_.name + ": " + error);
|
||||
}
|
||||
|
||||
line_fd_ = request.fd;
|
||||
if (line_fd_ < 0) {
|
||||
::close(chip_fd_);
|
||||
chip_fd_ = -1;
|
||||
throw std::runtime_error("GPIO line request returned invalid fd for " + settings_.name);
|
||||
}
|
||||
}
|
||||
|
||||
void H7992MinimalDriver::close_native() {
|
||||
if (line_fd_ >= 0) {
|
||||
::close(line_fd_);
|
||||
line_fd_ = -1;
|
||||
}
|
||||
if (chip_fd_ >= 0) {
|
||||
::close(chip_fd_);
|
||||
chip_fd_ = -1;
|
||||
}
|
||||
}
|
||||
|
||||
void H7992MinimalDriver::switch_native(std::uint32_t position) {
|
||||
if (line_fd_ < 0) {
|
||||
throw std::runtime_error("Native GPIO line fd is not open for " + settings_.name);
|
||||
}
|
||||
|
||||
const auto [pin_a_state, pin_b_state] = kPositionToAB[position];
|
||||
auto values = make_line_values(pin_a_state, pin_b_state);
|
||||
|
||||
if (::ioctl(line_fd_, GPIO_V2_LINE_SET_VALUES_IOCTL, &values) != 0) {
|
||||
throw std::runtime_error("Failed to switch GPIO state for " + settings_.name + ": " + std::strerror(errno));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace radar::drivers
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
#include "run_config.hpp"
|
||||
#include "switch_driver.hpp"
|
||||
|
||||
namespace radar::drivers {
|
||||
|
||||
/**
|
||||
* @brief Runtime settings for the H7992 GPIO switch driver.
|
||||
*/
|
||||
struct H7992MinimalDriverSettings {
|
||||
std::string name{};
|
||||
config::DriverMode mode = config::DriverMode::Native;
|
||||
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;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Minimal H7992 implementation for fast position switching.
|
||||
*
|
||||
* Native mode uses Linux GPIO character-device API (uAPI v2).
|
||||
* Mock mode keeps state in memory without touching hardware.
|
||||
*/
|
||||
class H7992MinimalDriver final : public SwitchDriver {
|
||||
public:
|
||||
explicit H7992MinimalDriver(H7992MinimalDriverSettings settings);
|
||||
|
||||
void open() override;
|
||||
void close() override;
|
||||
[[nodiscard]] auto position_count() const -> std::uint32_t override;
|
||||
void switch_to(std::uint32_t position) override;
|
||||
|
||||
private:
|
||||
void open_native();
|
||||
void close_native();
|
||||
void switch_native(std::uint32_t position);
|
||||
|
||||
H7992MinimalDriverSettings settings_{};
|
||||
bool is_open_ = false;
|
||||
std::uint32_t current_position_ = 0;
|
||||
int chip_fd_ = -1;
|
||||
int line_fd_ = -1;
|
||||
};
|
||||
|
||||
} // namespace radar::drivers
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
#include "hmc349a_minimal_driver.hpp"
|
||||
|
||||
#include <cerrno>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <fcntl.h>
|
||||
#include <linux/gpio.h>
|
||||
#include <stdexcept>
|
||||
#include <sys/ioctl.h>
|
||||
#include <unistd.h>
|
||||
#include <utility>
|
||||
|
||||
namespace radar::drivers {
|
||||
namespace {
|
||||
|
||||
void validate_open_settings(const HMC349AMinimalDriverSettings& settings) {
|
||||
if (settings.positions == 0U || settings.positions > 2U) {
|
||||
throw std::runtime_error("HMC349A switch positions must be in range [1, 2] for " + settings.name);
|
||||
}
|
||||
if (settings.default_position >= settings.positions) {
|
||||
throw std::runtime_error("Switch default_position out of range for " + settings.name);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
HMC349AMinimalDriver::HMC349AMinimalDriver(HMC349AMinimalDriverSettings settings)
|
||||
: settings_(std::move(settings)) {}
|
||||
|
||||
void HMC349AMinimalDriver::open() {
|
||||
if (is_open_) {
|
||||
return;
|
||||
}
|
||||
|
||||
validate_open_settings(settings_);
|
||||
|
||||
if (settings_.mode == config::DriverMode::Native) {
|
||||
open_native();
|
||||
}
|
||||
|
||||
is_open_ = true;
|
||||
switch_to(settings_.default_position);
|
||||
}
|
||||
|
||||
void HMC349AMinimalDriver::close() {
|
||||
if (!is_open_) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (settings_.mode == config::DriverMode::Native) {
|
||||
close_native();
|
||||
}
|
||||
|
||||
is_open_ = false;
|
||||
}
|
||||
|
||||
auto HMC349AMinimalDriver::position_count() const -> std::uint32_t {
|
||||
return settings_.positions;
|
||||
}
|
||||
|
||||
void HMC349AMinimalDriver::switch_to(std::uint32_t position) {
|
||||
if (!is_open_) {
|
||||
throw std::runtime_error("Switch driver is not open for " + settings_.name);
|
||||
}
|
||||
if (position >= settings_.positions) {
|
||||
throw std::runtime_error("Switch position out of range for " + settings_.name);
|
||||
}
|
||||
|
||||
switch (settings_.mode) {
|
||||
case config::DriverMode::Mock:
|
||||
break;
|
||||
case config::DriverMode::Native:
|
||||
switch_native(position);
|
||||
break;
|
||||
default:
|
||||
throw std::runtime_error("Unsupported switch driver mode for " + settings_.name);
|
||||
}
|
||||
|
||||
current_position_ = position;
|
||||
}
|
||||
|
||||
void HMC349AMinimalDriver::open_native() {
|
||||
if (settings_.gpio_chip.empty()) {
|
||||
throw std::runtime_error("gpio_chip is empty for native switch " + settings_.name);
|
||||
}
|
||||
if (settings_.pin_a < 0) {
|
||||
throw std::runtime_error("pin_a (control) is invalid for native switch " + settings_.name);
|
||||
}
|
||||
|
||||
chip_fd_ = ::open(settings_.gpio_chip.c_str(), O_RDONLY | O_CLOEXEC);
|
||||
if (chip_fd_ < 0) {
|
||||
throw std::runtime_error(
|
||||
"Failed to open gpio chip '" + settings_.gpio_chip + "' for " + settings_.name + ": " + std::strerror(errno)
|
||||
);
|
||||
}
|
||||
|
||||
gpio_v2_line_request request{};
|
||||
// Bit 0 controls pin_a.
|
||||
request.offsets[0] = static_cast<std::uint32_t>(settings_.pin_a);
|
||||
request.num_lines = 1;
|
||||
request.config.flags = GPIO_V2_LINE_FLAG_OUTPUT;
|
||||
std::snprintf(request.consumer, sizeof(request.consumer), "radar_%s", settings_.name.c_str());
|
||||
|
||||
if (::ioctl(chip_fd_, GPIO_V2_GET_LINE_IOCTL, &request) != 0) {
|
||||
const auto error = std::strerror(errno);
|
||||
::close(chip_fd_);
|
||||
chip_fd_ = -1;
|
||||
throw std::runtime_error("Failed to request GPIO lines for " + settings_.name + ": " + error);
|
||||
}
|
||||
|
||||
line_fd_ = request.fd;
|
||||
if (line_fd_ < 0) {
|
||||
::close(chip_fd_);
|
||||
chip_fd_ = -1;
|
||||
throw std::runtime_error("GPIO line request returned invalid fd for " + settings_.name);
|
||||
}
|
||||
}
|
||||
|
||||
void HMC349AMinimalDriver::close_native() {
|
||||
if (line_fd_ >= 0) {
|
||||
::close(line_fd_);
|
||||
line_fd_ = -1;
|
||||
}
|
||||
if (chip_fd_ >= 0) {
|
||||
::close(chip_fd_);
|
||||
chip_fd_ = -1;
|
||||
}
|
||||
}
|
||||
|
||||
void HMC349AMinimalDriver::switch_native(std::uint32_t position) {
|
||||
if (line_fd_ < 0) {
|
||||
throw std::runtime_error("Native GPIO line fd is not open for " + settings_.name);
|
||||
}
|
||||
|
||||
const std::uint8_t requested_state = static_cast<std::uint8_t>(position & 0x01U);
|
||||
const std::uint8_t control_state = settings_.invert_logic ? (requested_state ^ 0x01U) : requested_state;
|
||||
|
||||
gpio_v2_line_values values{};
|
||||
values.mask = (1ULL << 0U);
|
||||
values.bits = static_cast<std::uint64_t>(control_state);
|
||||
|
||||
if (::ioctl(line_fd_, GPIO_V2_LINE_SET_VALUES_IOCTL, &values) != 0) {
|
||||
throw std::runtime_error("Failed to switch GPIO state for " + settings_.name + ": " + std::strerror(errno));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace radar::drivers
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
#include "run_config.hpp"
|
||||
#include "switch_driver.hpp"
|
||||
|
||||
namespace radar::drivers {
|
||||
|
||||
/**
|
||||
* @brief Runtime settings for the HMC349A GPIO switch driver.
|
||||
*/
|
||||
struct HMC349AMinimalDriverSettings {
|
||||
std::string name{};
|
||||
config::DriverMode mode = config::DriverMode::Native;
|
||||
std::uint32_t positions = 2;
|
||||
std::uint32_t default_position = 0;
|
||||
std::string gpio_chip = "/dev/gpiochip0";
|
||||
std::int32_t pin_a = 17; ///< Control pin.
|
||||
bool invert_logic = false; ///< Invert control logic for position mapping.
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Minimal HMC349A implementation for deterministic GPIO control.
|
||||
*
|
||||
* Native mode uses Linux GPIO character-device API (uAPI v2).
|
||||
* Mock mode keeps state in memory without touching hardware.
|
||||
*/
|
||||
class HMC349AMinimalDriver final : public SwitchDriver {
|
||||
public:
|
||||
explicit HMC349AMinimalDriver(HMC349AMinimalDriverSettings settings);
|
||||
|
||||
void open() override;
|
||||
void close() override;
|
||||
[[nodiscard]] auto position_count() const -> std::uint32_t override;
|
||||
void switch_to(std::uint32_t position) override;
|
||||
|
||||
private:
|
||||
void open_native();
|
||||
void close_native();
|
||||
void switch_native(std::uint32_t position);
|
||||
|
||||
HMC349AMinimalDriverSettings settings_{};
|
||||
bool is_open_ = false;
|
||||
std::uint32_t current_position_ = 0;
|
||||
|
||||
int chip_fd_ = -1;
|
||||
int line_fd_ = -1;
|
||||
};
|
||||
|
||||
} // namespace radar::drivers
|
||||
@@ -0,0 +1,62 @@
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
|
||||
#include "radar_driver.hpp"
|
||||
#include "run_config.hpp"
|
||||
#include "shm_ring.hpp"
|
||||
#include "sweep_plan.hpp"
|
||||
#include "switch_driver.hpp"
|
||||
|
||||
namespace radar::acq {
|
||||
|
||||
/**
|
||||
* @brief Produces raw sweep collections for configured switch combinations.
|
||||
*
|
||||
* Lifecycle:
|
||||
* 1. Open radar and switch drivers.
|
||||
* 2. Iterate configured combinations, acquire one sweep per combination.
|
||||
* 3. Publish serialized collections into the raw shared-memory ring.
|
||||
*/
|
||||
class SweepOrchestrator {
|
||||
public:
|
||||
/**
|
||||
* @param config Runtime config loaded from JSON.
|
||||
* @param radar_driver Radar device implementation.
|
||||
* @param input_switch_driver Switch connected to radar input path.
|
||||
* @param output_switch_driver Switch connected to radar output path.
|
||||
* @param raw_ring Shared-memory ring for raw collections.
|
||||
* @param raw_tap_ring Optional tap ring for GUI/debug readers.
|
||||
*/
|
||||
SweepOrchestrator(
|
||||
const config::RunConfig& config,
|
||||
drivers::RadarDriver& radar_driver,
|
||||
drivers::SwitchDriver& input_switch_driver,
|
||||
drivers::SwitchDriver& output_switch_driver,
|
||||
ipc::ShmRing& raw_ring,
|
||||
ipc::ShmRing* raw_tap_ring = nullptr
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Run acquisition loop until stop is requested or single run completes.
|
||||
*/
|
||||
void run(const std::atomic<bool>& stop_requested);
|
||||
|
||||
private:
|
||||
/**
|
||||
* @brief Acquire one collection for all combinations in `plan_`.
|
||||
*/
|
||||
[[nodiscard]] auto acquire_one_collection(std::uint64_t collection_id, const std::atomic<bool>& stop_requested)
|
||||
-> ipc::RawSweepCollection;
|
||||
|
||||
const config::RunConfig& config_;
|
||||
drivers::RadarDriver& radar_driver_;
|
||||
drivers::SwitchDriver& input_switch_driver_;
|
||||
drivers::SwitchDriver& output_switch_driver_;
|
||||
ipc::ShmRing& raw_ring_;
|
||||
ipc::ShmRing* raw_tap_ring_ = nullptr;
|
||||
SweepPlan plan_{};
|
||||
};
|
||||
|
||||
} // namespace radar::acq
|
||||
@@ -0,0 +1,22 @@
|
||||
#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
|
||||
@@ -0,0 +1,135 @@
|
||||
#include <atomic>
|
||||
#include <csignal>
|
||||
#include <exception>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
|
||||
#include "h7992_minimal_driver.hpp"
|
||||
#include "hmc349a_minimal_driver.hpp"
|
||||
#include "librevna_minimal_driver.hpp"
|
||||
#include "run_config.hpp"
|
||||
#include "shm_ring.hpp"
|
||||
#include "sweep_orchestrator.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr const char* kDefaultConfigPath = "run_config.json";
|
||||
std::atomic<bool> g_stop_requested{false};
|
||||
|
||||
void signal_handler(int /*signal*/) {
|
||||
g_stop_requested.store(true, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void install_signal_handlers() {
|
||||
std::signal(SIGINT, signal_handler);
|
||||
std::signal(SIGTERM, signal_handler);
|
||||
}
|
||||
|
||||
[[nodiscard]] auto read_config_path(int argc, char** argv) -> std::string {
|
||||
std::string config_path = kDefaultConfigPath;
|
||||
|
||||
for (int index = 1; index < argc; ++index) {
|
||||
const std::string arg = argv[index];
|
||||
if (arg == "--config" && (index + 1) < argc) {
|
||||
config_path = argv[++index];
|
||||
}
|
||||
}
|
||||
|
||||
return config_path;
|
||||
}
|
||||
|
||||
[[nodiscard]] auto make_radar_driver(const radar::config::RadarConfig& config) -> radar::drivers::LibreVnaMinimalDriver {
|
||||
return radar::drivers::LibreVnaMinimalDriver({
|
||||
.mode = config.driver_mode,
|
||||
.serial = config.serial,
|
||||
.sweep = config.sweep,
|
||||
.mock_signal_hz = config.mock_signal_hz,
|
||||
});
|
||||
}
|
||||
|
||||
[[nodiscard]] auto make_h7992_driver(const radar::config::SwitchConfig& config)
|
||||
-> std::unique_ptr<radar::drivers::SwitchDriver> {
|
||||
return std::make_unique<radar::drivers::H7992MinimalDriver>(
|
||||
radar::drivers::H7992MinimalDriverSettings{
|
||||
.name = config.name,
|
||||
.mode = config.driver_mode,
|
||||
.positions = config.positions,
|
||||
.default_position = config.default_position,
|
||||
.gpio_chip = config.gpio_chip,
|
||||
.pin_a = config.pin_a,
|
||||
.pin_b = config.pin_b,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
[[nodiscard]] auto make_hmc349a_driver(const radar::config::SwitchConfig& config)
|
||||
-> std::unique_ptr<radar::drivers::SwitchDriver> {
|
||||
return std::make_unique<radar::drivers::HMC349AMinimalDriver>(
|
||||
radar::drivers::HMC349AMinimalDriverSettings{
|
||||
.name = config.name,
|
||||
.mode = config.driver_mode,
|
||||
.positions = config.positions,
|
||||
.default_position = config.default_position,
|
||||
.gpio_chip = config.gpio_chip,
|
||||
.pin_a = config.pin_a,
|
||||
.invert_logic = config.invert_logic,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
[[nodiscard]] auto make_switch_driver(const radar::config::SwitchConfig& config)
|
||||
-> std::unique_ptr<radar::drivers::SwitchDriver> {
|
||||
using radar::config::SwitchDriverKind;
|
||||
|
||||
switch (config.driver_kind) {
|
||||
case SwitchDriverKind::H7992:
|
||||
return make_h7992_driver(config);
|
||||
case SwitchDriverKind::HMC349A:
|
||||
return make_hmc349a_driver(config);
|
||||
default:
|
||||
throw std::runtime_error("Unsupported switch driver kind");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
install_signal_handlers();
|
||||
|
||||
try {
|
||||
const auto config_path = read_config_path(argc, argv);
|
||||
const auto config = radar::config::load_run_config(config_path);
|
||||
|
||||
auto raw_ring = radar::ipc::ShmRing::open_or_create(
|
||||
config.rings.raw.name,
|
||||
config.rings.raw.capacity,
|
||||
config.rings.raw.slot_size_bytes
|
||||
);
|
||||
auto raw_tap_ring = radar::ipc::ShmRing::open_or_create(
|
||||
config.rings.raw_tap.name,
|
||||
config.rings.raw_tap.capacity,
|
||||
config.rings.raw_tap.slot_size_bytes
|
||||
);
|
||||
|
||||
auto radar_driver = make_radar_driver(config.radar);
|
||||
|
||||
auto input_switch = make_switch_driver(config.input_switch);
|
||||
auto output_switch = make_switch_driver(config.output_switch);
|
||||
|
||||
radar::acq::SweepOrchestrator orchestrator(
|
||||
config,
|
||||
radar_driver,
|
||||
*input_switch,
|
||||
*output_switch,
|
||||
raw_ring,
|
||||
&raw_tap_ring
|
||||
);
|
||||
orchestrator.run(g_stop_requested);
|
||||
return 0;
|
||||
} catch (const std::exception& exception) {
|
||||
std::cerr << "sweep_orchestrator error: " << exception.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
#include "sweep_orchestrator.hpp"
|
||||
|
||||
#include <chrono>
|
||||
#include <exception>
|
||||
#include <stdexcept>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
namespace radar::acq {
|
||||
namespace {
|
||||
|
||||
[[nodiscard]] inline auto should_stop(const std::atomic<bool>& stop_requested) -> bool {
|
||||
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));
|
||||
}
|
||||
|
||||
void validate_sweep(const drivers::SweepTrace& sweep) {
|
||||
if (sweep.frequency_hz.size() != sweep.s21.size()) {
|
||||
throw std::runtime_error("Radar driver returned inconsistent sweep vectors");
|
||||
}
|
||||
}
|
||||
|
||||
class DriverLifecycleGuard {
|
||||
public:
|
||||
DriverLifecycleGuard(
|
||||
drivers::RadarDriver& radar_driver,
|
||||
drivers::SwitchDriver& input_switch_driver,
|
||||
drivers::SwitchDriver& output_switch_driver
|
||||
)
|
||||
: radar_driver_(radar_driver),
|
||||
input_switch_driver_(input_switch_driver),
|
||||
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 (...) {
|
||||
}
|
||||
}
|
||||
|
||||
void open_all() {
|
||||
radar_driver_.open();
|
||||
input_switch_driver_.open();
|
||||
output_switch_driver_.open();
|
||||
active_ = true;
|
||||
}
|
||||
|
||||
void close_all() {
|
||||
if (!active_) {
|
||||
return;
|
||||
}
|
||||
output_switch_driver_.close();
|
||||
input_switch_driver_.close();
|
||||
radar_driver_.close();
|
||||
active_ = false;
|
||||
}
|
||||
|
||||
private:
|
||||
drivers::RadarDriver& radar_driver_;
|
||||
drivers::SwitchDriver& input_switch_driver_;
|
||||
drivers::SwitchDriver& output_switch_driver_;
|
||||
bool active_ = false;
|
||||
};
|
||||
|
||||
void publish_collection(ipc::ShmRing& raw_ring, ipc::ShmRing* raw_tap_ring, const ipc::RawSweepCollection& collection) {
|
||||
const auto serialized_collection = ipc::serialize_raw_collection(collection);
|
||||
if (!raw_ring.push(serialized_collection)) {
|
||||
throw std::runtime_error("Raw ring slot is too small for serialized collection");
|
||||
}
|
||||
if (raw_tap_ring != nullptr && !raw_tap_ring->push(serialized_collection)) {
|
||||
throw std::runtime_error("Raw tap ring slot is too small for serialized collection");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
SweepOrchestrator::SweepOrchestrator(
|
||||
const config::RunConfig& config,
|
||||
drivers::RadarDriver& radar_driver,
|
||||
drivers::SwitchDriver& input_switch_driver,
|
||||
drivers::SwitchDriver& output_switch_driver,
|
||||
ipc::ShmRing& raw_ring,
|
||||
ipc::ShmRing* raw_tap_ring
|
||||
)
|
||||
: config_(config),
|
||||
radar_driver_(radar_driver),
|
||||
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)) {}
|
||||
|
||||
void SweepOrchestrator::run(const std::atomic<bool>& stop_requested) {
|
||||
DriverLifecycleGuard lifecycle_guard(radar_driver_, input_switch_driver_, output_switch_driver_);
|
||||
// Open devices once and keep them active for the whole acquisition loop.
|
||||
lifecycle_guard.open_all();
|
||||
|
||||
std::uint64_t collection_id = 0;
|
||||
while (!should_stop(stop_requested)) {
|
||||
auto raw_collection = acquire_one_collection(++collection_id, stop_requested);
|
||||
if (raw_collection.traces.empty()) {
|
||||
if (!config_.runtime.continuous) {
|
||||
break;
|
||||
}
|
||||
sleep_if_needed_ms(config_.runtime.idle_sleep_ms);
|
||||
continue;
|
||||
}
|
||||
|
||||
publish_collection(raw_ring_, raw_tap_ring_, raw_collection);
|
||||
|
||||
if (!config_.runtime.continuous) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
lifecycle_guard.close_all();
|
||||
}
|
||||
|
||||
auto SweepOrchestrator::acquire_one_collection(
|
||||
std::uint64_t collection_id,
|
||||
const std::atomic<bool>& stop_requested
|
||||
) -> ipc::RawSweepCollection {
|
||||
ipc::RawSweepCollection collection{};
|
||||
collection.collection_id = collection_id;
|
||||
collection.monotonic_ns = ipc::current_monotonic_ns();
|
||||
collection.traces.reserve(plan_.ordered_combos.size());
|
||||
bool interrupted = false;
|
||||
|
||||
for (const auto& combo : plan_.ordered_combos) {
|
||||
if (should_stop(stop_requested)) {
|
||||
interrupted = true;
|
||||
break;
|
||||
}
|
||||
|
||||
// Hardware flow for each combo:
|
||||
// 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);
|
||||
|
||||
auto sweep = radar_driver_.acquire_s21_sweep();
|
||||
validate_sweep(sweep);
|
||||
|
||||
ipc::SweepTraceBlock trace{};
|
||||
trace.combo = combo;
|
||||
trace.frequency_hz = std::move(sweep.frequency_hz);
|
||||
trace.s21 = std::move(sweep.s21);
|
||||
collection.traces.push_back(std::move(trace));
|
||||
}
|
||||
|
||||
if (interrupted) {
|
||||
// Do not emit partial collections when stop was requested mid-cycle.
|
||||
collection.traces.clear();
|
||||
}
|
||||
|
||||
return collection;
|
||||
}
|
||||
|
||||
} // namespace radar::acq
|
||||
@@ -0,0 +1,19 @@
|
||||
#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