init commit

This commit is contained in:
Ayzen
2026-03-05 14:42:33 +03:00
commit fd4618b20d
964 changed files with 325114 additions and 0 deletions
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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