added remote k209 setup
This commit is contained in:
@@ -32,6 +32,7 @@ ORCH_SOURCES := \
|
|||||||
data_acq_and_processing/sweep_orchestrator/device_drivers/radar/librevna/librevna_minimal_driver_lifecycle.cpp \
|
data_acq_and_processing/sweep_orchestrator/device_drivers/radar/librevna/librevna_minimal_driver_lifecycle.cpp \
|
||||||
data_acq_and_processing/sweep_orchestrator/device_drivers/radar/librevna/librevna_minimal_driver_transport.cpp \
|
data_acq_and_processing/sweep_orchestrator/device_drivers/radar/librevna/librevna_minimal_driver_transport.cpp \
|
||||||
data_acq_and_processing/sweep_orchestrator/device_drivers/radar/librevna/librevna_minimal_driver_protocol.cpp \
|
data_acq_and_processing/sweep_orchestrator/device_drivers/radar/librevna/librevna_minimal_driver_protocol.cpp \
|
||||||
|
data_acq_and_processing/sweep_orchestrator/device_drivers/radar/remote_compact_m_k209/remote_compact_m_k209_driver.cpp \
|
||||||
data_acq_and_processing/sweep_orchestrator/device_drivers/switches/h7992_minimal_driver.cpp \
|
data_acq_and_processing/sweep_orchestrator/device_drivers/switches/h7992_minimal_driver.cpp \
|
||||||
data_acq_and_processing/sweep_orchestrator/device_drivers/switches/hmc349a_minimal_driver.cpp \
|
data_acq_and_processing/sweep_orchestrator/device_drivers/switches/hmc349a_minimal_driver.cpp \
|
||||||
data_acq_and_processing/sweep_orchestrator/src/sweep_orchestrator.cpp \
|
data_acq_and_processing/sweep_orchestrator/src/sweep_orchestrator.cpp \
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# radar_system
|
||||||
|
|
||||||
|
Radar acquisition and processing system for single LibreVNA, synchronized
|
||||||
|
multi-device LibreVNA, and Compact-M K209/S2VNA setups.
|
||||||
|
|
||||||
|
Start here:
|
||||||
|
|
||||||
|
- [Operation Modes](docs/operation_modes.md): what to run on each machine for
|
||||||
|
`librevna`, `librevna_multi`, and `compact_m_k209`.
|
||||||
|
- [Run Config Reference](docs/run_config.md): `run_config.json` fields and
|
||||||
|
example files.
|
||||||
|
- [K209 Setup](docs/k209_setup.md): S2VNA, VISA, K209 limits, smoke tests, and
|
||||||
|
Raspberry Pi remote mode details.
|
||||||
|
|
||||||
|
Common local setup:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /path/to/radar_system
|
||||||
|
python3 -m venv .venv
|
||||||
|
.venv/bin/python -m pip install --upgrade pip
|
||||||
|
.venv/bin/python -m pip install -r requirements.txt
|
||||||
|
make
|
||||||
|
```
|
||||||
|
|
||||||
|
Run the GUI:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
.venv/bin/python -m python_app.gui.main
|
||||||
|
```
|
||||||
|
|
||||||
@@ -31,6 +31,8 @@ struct RadarConfig {
|
|||||||
// Radar device identity and runtime mode.
|
// Radar device identity and runtime mode.
|
||||||
std::string model = "librevna";
|
std::string model = "librevna";
|
||||||
std::string serial{};
|
std::string serial{};
|
||||||
|
std::string remote_host = "127.0.0.1";
|
||||||
|
std::uint32_t remote_port = 50209;
|
||||||
DriverMode driver_mode = DriverMode::Mock;
|
DriverMode driver_mode = DriverMode::Mock;
|
||||||
float mock_signal_hz = 5'000'000.0F;
|
float mock_signal_hz = 5'000'000.0F;
|
||||||
RadarSweepSettings sweep{};
|
RadarSweepSettings sweep{};
|
||||||
|
|||||||
@@ -413,6 +413,14 @@ auto load_run_config(const std::string& path) -> RunConfig {
|
|||||||
const auto* radar_obj = as_object(required_field(*root_obj, "radar"), "radar");
|
const auto* radar_obj = as_object(required_field(*root_obj, "radar"), "radar");
|
||||||
config.radar.model = optional_string(*radar_obj, "model", "librevna");
|
config.radar.model = optional_string(*radar_obj, "model", "librevna");
|
||||||
config.radar.serial = optional_string(*radar_obj, "serial", "");
|
config.radar.serial = optional_string(*radar_obj, "serial", "");
|
||||||
|
config.radar.remote_host = optional_string(*radar_obj, "remote_host", "127.0.0.1");
|
||||||
|
config.radar.remote_port = optional_u32(*radar_obj, "remote_port", 50209);
|
||||||
|
if (config.radar.remote_host.empty()) {
|
||||||
|
throw std::runtime_error("radar.remote_host must not be empty");
|
||||||
|
}
|
||||||
|
if (config.radar.remote_port == 0U || config.radar.remote_port > 65535U) {
|
||||||
|
throw std::runtime_error("radar.remote_port must be in 1..65535");
|
||||||
|
}
|
||||||
config.radar.driver_mode = parse_driver_mode(optional_string(*radar_obj, "driver_mode", "mock"));
|
config.radar.driver_mode = parse_driver_mode(optional_string(*radar_obj, "driver_mode", "mock"));
|
||||||
config.radar.mock_signal_hz = optional_f32(*radar_obj, "mock_signal_hz", 5'000'000.0F);
|
config.radar.mock_signal_hz = optional_f32(*radar_obj, "mock_signal_hz", 5'000'000.0F);
|
||||||
|
|
||||||
|
|||||||
+271
@@ -0,0 +1,271 @@
|
|||||||
|
#include "../remote_compact_m_k209_driver.hpp"
|
||||||
|
|
||||||
|
#include <arpa/inet.h>
|
||||||
|
#include <netdb.h>
|
||||||
|
#include <netinet/tcp.h>
|
||||||
|
#include <sys/socket.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#include <cerrno>
|
||||||
|
#include <cstring>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <string>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace radar::drivers {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr std::uint8_t kCommandConfigure = static_cast<std::uint8_t>('C');
|
||||||
|
constexpr std::uint8_t kCommandAcquire = static_cast<std::uint8_t>('A');
|
||||||
|
constexpr std::uint8_t kStatusOk = static_cast<std::uint8_t>('O');
|
||||||
|
constexpr std::uint8_t kStatusError = static_cast<std::uint8_t>('E');
|
||||||
|
|
||||||
|
[[nodiscard]] auto system_error_message(const std::string& context) -> std::string {
|
||||||
|
return context + ": " + std::strerror(errno);
|
||||||
|
}
|
||||||
|
|
||||||
|
void append_u32_be(std::vector<std::uint8_t>& buffer, std::uint32_t value) {
|
||||||
|
buffer.push_back(static_cast<std::uint8_t>((value >> 24U) & 0xFFU));
|
||||||
|
buffer.push_back(static_cast<std::uint8_t>((value >> 16U) & 0xFFU));
|
||||||
|
buffer.push_back(static_cast<std::uint8_t>((value >> 8U) & 0xFFU));
|
||||||
|
buffer.push_back(static_cast<std::uint8_t>(value & 0xFFU));
|
||||||
|
}
|
||||||
|
|
||||||
|
void append_u64_be(std::vector<std::uint8_t>& buffer, std::uint64_t value) {
|
||||||
|
for (int shift = 56; shift >= 0; shift -= 8) {
|
||||||
|
buffer.push_back(static_cast<std::uint8_t>((value >> static_cast<unsigned>(shift)) & 0xFFU));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void append_double_be(std::vector<std::uint8_t>& buffer, double value) {
|
||||||
|
std::uint64_t bits = 0;
|
||||||
|
static_assert(sizeof(bits) == sizeof(value));
|
||||||
|
std::memcpy(&bits, &value, sizeof(bits));
|
||||||
|
append_u64_be(buffer, bits);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] auto parse_u32_be(const std::uint8_t* data) -> std::uint32_t {
|
||||||
|
return (static_cast<std::uint32_t>(data[0]) << 24U) |
|
||||||
|
(static_cast<std::uint32_t>(data[1]) << 16U) |
|
||||||
|
(static_cast<std::uint32_t>(data[2]) << 8U) |
|
||||||
|
static_cast<std::uint32_t>(data[3]);
|
||||||
|
}
|
||||||
|
|
||||||
|
void send_all(int socket_fd, const void* data, std::size_t size) {
|
||||||
|
const auto* cursor = static_cast<const std::uint8_t*>(data);
|
||||||
|
auto remaining = size;
|
||||||
|
while (remaining > 0U) {
|
||||||
|
const auto sent = ::send(socket_fd, cursor, remaining, MSG_NOSIGNAL);
|
||||||
|
if (sent < 0) {
|
||||||
|
throw std::runtime_error(system_error_message("send to K209 remote server failed"));
|
||||||
|
}
|
||||||
|
if (sent == 0) {
|
||||||
|
throw std::runtime_error("send to K209 remote server returned zero bytes");
|
||||||
|
}
|
||||||
|
cursor += sent;
|
||||||
|
remaining -= static_cast<std::size_t>(sent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void recv_exact(int socket_fd, void* data, std::size_t size) {
|
||||||
|
auto* cursor = static_cast<std::uint8_t*>(data);
|
||||||
|
auto remaining = size;
|
||||||
|
while (remaining > 0U) {
|
||||||
|
const auto received = ::recv(socket_fd, cursor, remaining, 0);
|
||||||
|
if (received < 0) {
|
||||||
|
throw std::runtime_error(system_error_message("read from K209 remote server failed"));
|
||||||
|
}
|
||||||
|
if (received == 0) {
|
||||||
|
throw std::runtime_error("K209 remote server closed the connection");
|
||||||
|
}
|
||||||
|
cursor += received;
|
||||||
|
remaining -= static_cast<std::size_t>(received);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] auto recv_u32(int socket_fd) -> std::uint32_t {
|
||||||
|
std::uint8_t bytes[4]{};
|
||||||
|
recv_exact(socket_fd, bytes, sizeof(bytes));
|
||||||
|
return parse_u32_be(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
void read_status(int socket_fd) {
|
||||||
|
std::uint8_t status = 0;
|
||||||
|
recv_exact(socket_fd, &status, 1);
|
||||||
|
if (status == kStatusOk) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (status == kStatusError) {
|
||||||
|
const auto message_size = recv_u32(socket_fd);
|
||||||
|
std::string message(message_size, '\0');
|
||||||
|
if (message_size > 0U) {
|
||||||
|
recv_exact(socket_fd, message.data(), message.size());
|
||||||
|
}
|
||||||
|
throw std::runtime_error("K209 remote server error: " + message);
|
||||||
|
}
|
||||||
|
throw std::runtime_error("K209 remote server returned invalid status byte");
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] auto recv_float32_array(int socket_fd, std::uint32_t expected_values, const std::string& context)
|
||||||
|
-> std::vector<float> {
|
||||||
|
const auto payload_size = recv_u32(socket_fd);
|
||||||
|
const auto expected_size = expected_values * static_cast<std::uint32_t>(sizeof(float));
|
||||||
|
if (payload_size != expected_size) {
|
||||||
|
throw std::runtime_error(
|
||||||
|
"K209 remote " + context + " payload has " + std::to_string(payload_size) +
|
||||||
|
" bytes, expected " + std::to_string(expected_size)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<float> values(expected_values);
|
||||||
|
if (payload_size > 0U) {
|
||||||
|
recv_exact(socket_fd, values.data(), payload_size);
|
||||||
|
}
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] auto connect_socket(const std::string& host, std::uint32_t port, std::uint32_t timeout_ms) -> int {
|
||||||
|
addrinfo hints{};
|
||||||
|
hints.ai_family = AF_UNSPEC;
|
||||||
|
hints.ai_socktype = SOCK_STREAM;
|
||||||
|
hints.ai_protocol = IPPROTO_TCP;
|
||||||
|
|
||||||
|
addrinfo* result = nullptr;
|
||||||
|
const auto port_text = std::to_string(port);
|
||||||
|
const auto rc = ::getaddrinfo(host.c_str(), port_text.c_str(), &hints, &result);
|
||||||
|
if (rc != 0) {
|
||||||
|
throw std::runtime_error("getaddrinfo failed for K209 remote host " + host + ": " + ::gai_strerror(rc));
|
||||||
|
}
|
||||||
|
|
||||||
|
int connected_fd = -1;
|
||||||
|
std::string last_error;
|
||||||
|
for (auto* item = result; item != nullptr; item = item->ai_next) {
|
||||||
|
const int fd = ::socket(item->ai_family, item->ai_socktype, item->ai_protocol);
|
||||||
|
if (fd < 0) {
|
||||||
|
last_error = system_error_message("socket");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int one = 1;
|
||||||
|
static_cast<void>(::setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &one, sizeof(one)));
|
||||||
|
timeval timeout{};
|
||||||
|
timeout.tv_sec = static_cast<long>(timeout_ms / 1000U);
|
||||||
|
timeout.tv_usec = static_cast<long>((timeout_ms % 1000U) * 1000U);
|
||||||
|
static_cast<void>(::setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)));
|
||||||
|
static_cast<void>(::setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout)));
|
||||||
|
|
||||||
|
if (::connect(fd, item->ai_addr, item->ai_addrlen) == 0) {
|
||||||
|
connected_fd = fd;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
last_error = system_error_message("connect");
|
||||||
|
::close(fd);
|
||||||
|
}
|
||||||
|
::freeaddrinfo(result);
|
||||||
|
|
||||||
|
if (connected_fd < 0) {
|
||||||
|
throw std::runtime_error("Failed to connect to K209 remote server " + host + ":" + port_text + ": " + last_error);
|
||||||
|
}
|
||||||
|
return connected_fd;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] auto complex_from_interleaved(const std::vector<float>& values, std::uint32_t points, const std::string& context)
|
||||||
|
-> std::vector<ipc::Complex32> {
|
||||||
|
if (values.size() != static_cast<std::size_t>(points) * 2U) {
|
||||||
|
throw std::runtime_error("K209 remote " + context + " returned unexpected scalar count");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<ipc::Complex32> output(points);
|
||||||
|
for (std::uint32_t index = 0; index < points; ++index) {
|
||||||
|
output[index] = ipc::Complex32{
|
||||||
|
.re = values[static_cast<std::size_t>(index) * 2U],
|
||||||
|
.im = values[static_cast<std::size_t>(index) * 2U + 1U],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
RemoteCompactMK209Driver::RemoteCompactMK209Driver(RemoteCompactMK209DriverSettings settings)
|
||||||
|
: settings_(std::move(settings)) {
|
||||||
|
if (settings_.host.empty()) {
|
||||||
|
throw std::runtime_error("K209 remote host must not be empty");
|
||||||
|
}
|
||||||
|
if (settings_.port == 0U || settings_.port > 65535U) {
|
||||||
|
throw std::runtime_error("K209 remote port must be in 1..65535");
|
||||||
|
}
|
||||||
|
if (settings_.timeout_ms == 0U) {
|
||||||
|
throw std::runtime_error("K209 remote timeout_ms must be > 0");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RemoteCompactMK209Driver::~RemoteCompactMK209Driver() {
|
||||||
|
try {
|
||||||
|
close();
|
||||||
|
} catch (...) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void RemoteCompactMK209Driver::open() {
|
||||||
|
if (socket_fd_ >= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
socket_fd_ = connect_socket(settings_.host, settings_.port, settings_.timeout_ms);
|
||||||
|
|
||||||
|
try {
|
||||||
|
std::vector<std::uint8_t> payload{};
|
||||||
|
payload.reserve(1U + 8U + 8U + 4U + 8U + 8U);
|
||||||
|
payload.push_back(kCommandConfigure);
|
||||||
|
append_double_be(payload, static_cast<double>(settings_.sweep.start_hz));
|
||||||
|
append_double_be(payload, static_cast<double>(settings_.sweep.stop_hz));
|
||||||
|
append_u32_be(payload, settings_.sweep.points);
|
||||||
|
append_double_be(payload, static_cast<double>(settings_.sweep.if_bandwidth_hz));
|
||||||
|
append_double_be(payload, static_cast<double>(settings_.sweep.power_dbm));
|
||||||
|
send_all(socket_fd_, payload.data(), payload.size());
|
||||||
|
|
||||||
|
read_status(socket_fd_);
|
||||||
|
const auto points = recv_u32(socket_fd_);
|
||||||
|
if (points != settings_.sweep.points) {
|
||||||
|
throw std::runtime_error("K209 remote server returned unexpected configured point count");
|
||||||
|
}
|
||||||
|
frequency_hz_ = recv_float32_array(socket_fd_, points, "frequency");
|
||||||
|
} catch (...) {
|
||||||
|
close();
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void RemoteCompactMK209Driver::close() {
|
||||||
|
if (socket_fd_ >= 0) {
|
||||||
|
::close(socket_fd_);
|
||||||
|
socket_fd_ = -1;
|
||||||
|
}
|
||||||
|
frequency_hz_.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto RemoteCompactMK209Driver::acquire_sweep() -> SweepTrace {
|
||||||
|
if (socket_fd_ < 0) {
|
||||||
|
throw std::runtime_error("K209 remote driver is not open");
|
||||||
|
}
|
||||||
|
const std::uint8_t command = kCommandAcquire;
|
||||||
|
send_all(socket_fd_, &command, 1);
|
||||||
|
read_status(socket_fd_);
|
||||||
|
|
||||||
|
const auto points = recv_u32(socket_fd_);
|
||||||
|
if (points != settings_.sweep.points) {
|
||||||
|
throw std::runtime_error("K209 remote server returned unexpected sweep point count");
|
||||||
|
}
|
||||||
|
const auto s11_values = recv_float32_array(socket_fd_, points * 2U, "S11");
|
||||||
|
const auto s21_values = recv_float32_array(socket_fd_, points * 2U, "S21");
|
||||||
|
|
||||||
|
return SweepTrace{
|
||||||
|
.frequency_hz = frequency_hz_,
|
||||||
|
.s11 = complex_from_interleaved(s11_values, points, "S11"),
|
||||||
|
.s21 = complex_from_interleaved(s21_values, points, "S21"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace radar::drivers
|
||||||
+34
@@ -0,0 +1,34 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "radar_driver.hpp"
|
||||||
|
#include "run_config.hpp"
|
||||||
|
|
||||||
|
namespace radar::drivers {
|
||||||
|
|
||||||
|
struct RemoteCompactMK209DriverSettings {
|
||||||
|
std::string host = "127.0.0.1";
|
||||||
|
std::uint32_t port = 50209;
|
||||||
|
config::RadarSweepSettings sweep{};
|
||||||
|
std::uint32_t timeout_ms = 20'000;
|
||||||
|
};
|
||||||
|
|
||||||
|
class RemoteCompactMK209Driver final : public RadarDriver {
|
||||||
|
public:
|
||||||
|
explicit RemoteCompactMK209Driver(RemoteCompactMK209DriverSettings settings);
|
||||||
|
~RemoteCompactMK209Driver() override;
|
||||||
|
|
||||||
|
void open() override;
|
||||||
|
void close() override;
|
||||||
|
[[nodiscard]] auto acquire_sweep() -> SweepTrace override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
RemoteCompactMK209DriverSettings settings_{};
|
||||||
|
int socket_fd_ = -1;
|
||||||
|
std::vector<float> frequency_hz_{};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace radar::drivers
|
||||||
@@ -9,6 +9,8 @@
|
|||||||
#include "h7992_minimal_driver.hpp"
|
#include "h7992_minimal_driver.hpp"
|
||||||
#include "hmc349a_minimal_driver.hpp"
|
#include "hmc349a_minimal_driver.hpp"
|
||||||
#include "librevna_minimal_driver.hpp"
|
#include "librevna_minimal_driver.hpp"
|
||||||
|
#include "radar_driver.hpp"
|
||||||
|
#include "remote_compact_m_k209_driver.hpp"
|
||||||
#include "run_config.hpp"
|
#include "run_config.hpp"
|
||||||
#include "shm_ring.hpp"
|
#include "shm_ring.hpp"
|
||||||
#include "sweep_orchestrator.hpp"
|
#include "sweep_orchestrator.hpp"
|
||||||
@@ -16,6 +18,9 @@
|
|||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
constexpr const char* kDefaultConfigPath = "run_config.json";
|
constexpr const char* kDefaultConfigPath = "run_config.json";
|
||||||
|
constexpr const char* kLibreVnaModel = "librevna";
|
||||||
|
constexpr const char* kLibreVnaMultiModel = "librevna_multi";
|
||||||
|
constexpr const char* kCompactMK209Model = "compact_m_k209";
|
||||||
std::atomic<bool> g_stop_requested{false};
|
std::atomic<bool> g_stop_requested{false};
|
||||||
|
|
||||||
void signal_handler(int /*signal*/) {
|
void signal_handler(int /*signal*/) {
|
||||||
@@ -40,13 +45,38 @@ void install_signal_handlers() {
|
|||||||
return config_path;
|
return config_path;
|
||||||
}
|
}
|
||||||
|
|
||||||
[[nodiscard]] auto make_radar_driver(const radar::config::RadarConfig& config) -> radar::drivers::LibreVnaMinimalDriver {
|
[[nodiscard]] auto make_radar_driver(const radar::config::RadarConfig& config)
|
||||||
return radar::drivers::LibreVnaMinimalDriver({
|
-> std::unique_ptr<radar::drivers::RadarDriver> {
|
||||||
.mode = config.driver_mode,
|
if (config.model == kLibreVnaModel || config.model.empty()) {
|
||||||
.serial = config.serial,
|
return std::make_unique<radar::drivers::LibreVnaMinimalDriver>(
|
||||||
.sweep = config.sweep,
|
radar::drivers::LibreVnaMinimalDriverSettings{
|
||||||
.mock_signal_hz = config.mock_signal_hz,
|
.mode = config.driver_mode,
|
||||||
});
|
.serial = config.serial,
|
||||||
|
.sweep = config.sweep,
|
||||||
|
.mock_signal_hz = config.mock_signal_hz,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.model == kCompactMK209Model) {
|
||||||
|
if (config.driver_mode != radar::config::DriverMode::Native) {
|
||||||
|
throw std::runtime_error("compact_m_k209 requires radar.driver_mode='native'");
|
||||||
|
}
|
||||||
|
return std::make_unique<radar::drivers::RemoteCompactMK209Driver>(
|
||||||
|
radar::drivers::RemoteCompactMK209DriverSettings{
|
||||||
|
.host = config.remote_host,
|
||||||
|
.port = config.remote_port,
|
||||||
|
.sweep = config.sweep,
|
||||||
|
.timeout_ms = 20'000,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.model == kLibreVnaMultiModel) {
|
||||||
|
throw std::runtime_error("librevna_multi is handled by python_app.scripts.multi_device_raw_producer");
|
||||||
|
}
|
||||||
|
|
||||||
|
throw std::runtime_error("Unsupported radar.model for sweep_orchestrator: " + config.model);
|
||||||
}
|
}
|
||||||
|
|
||||||
[[nodiscard]] auto make_h7992_driver(const radar::config::SwitchConfig& config)
|
[[nodiscard]] auto make_h7992_driver(const radar::config::SwitchConfig& config)
|
||||||
@@ -120,7 +150,7 @@ int main(int argc, char** argv) {
|
|||||||
|
|
||||||
radar::acq::SweepOrchestrator orchestrator(
|
radar::acq::SweepOrchestrator orchestrator(
|
||||||
config,
|
config,
|
||||||
radar_driver,
|
*radar_driver,
|
||||||
*input_switch,
|
*input_switch,
|
||||||
*output_switch,
|
*output_switch,
|
||||||
raw_ring,
|
raw_ring,
|
||||||
|
|||||||
+33
-3
@@ -7,6 +7,11 @@ The production path is:
|
|||||||
K209 --USB-C--> S2VNA --HiSLIP/VISA--> radar_system
|
K209 --USB-C--> S2VNA --HiSLIP/VISA--> radar_system
|
||||||
```
|
```
|
||||||
|
|
||||||
|
For complete run-mode instructions, including what runs on the x86_64 S2VNA
|
||||||
|
computer and what runs on Raspberry Pi, see
|
||||||
|
[`docs/operation_modes.md`](operation_modes.md). For `run_config.json` fields,
|
||||||
|
see [`docs/run_config.md`](run_config.md).
|
||||||
|
|
||||||
There is no direct USB driver for K209 in this project. Do not use mock
|
There is no direct USB driver for K209 in this project. Do not use mock
|
||||||
transports, socket fallbacks, or `pyvisa-py` for the K209 path. The required
|
transports, socket fallbacks, or `pyvisa-py` for the K209 path. The required
|
||||||
transport dependency is an IVI/Vendor VISA implementation that provides both
|
transport dependency is an IVI/Vendor VISA implementation that provides both
|
||||||
@@ -140,6 +145,31 @@ TCPIP0::127.0.0.1::hislip0,4880::INSTR
|
|||||||
If S2VNA runs on another machine, replace `127.0.0.1` with that machine's IP
|
If S2VNA runs on another machine, replace `127.0.0.1` with that machine's IP
|
||||||
address.
|
address.
|
||||||
|
|
||||||
|
## Remote Raspberry Pi Mode
|
||||||
|
|
||||||
|
For Raspberry Pi runs, keep S2VNA and NI-VISA on the x86_64 computer connected
|
||||||
|
to the K209, and run only the project pipeline/GPIO on the Raspberry Pi.
|
||||||
|
|
||||||
|
On the x86_64 computer with S2VNA running:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /path/to/radar_system
|
||||||
|
.venv/bin/python -m python_app.scripts.k209_remote_server --host 0.0.0.0 --port 50209
|
||||||
|
```
|
||||||
|
|
||||||
|
On the Raspberry Pi, set the K209 config to the server address:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"radar": {
|
||||||
|
"model": "compact_m_k209",
|
||||||
|
"remote_host": "192.168.1.10",
|
||||||
|
"remote_port": 50209,
|
||||||
|
"driver_mode": "native"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The Raspberry Pi does not need S2VNA or NI-VISA for this mode.
|
||||||
|
|
||||||
## Python Smoke Test
|
## Python Smoke Test
|
||||||
|
|
||||||
Use the project virtual environment:
|
Use the project virtual environment:
|
||||||
@@ -263,9 +293,9 @@ are available for ARM64:
|
|||||||
TCPIP HiSLIP support.
|
TCPIP HiSLIP support.
|
||||||
|
|
||||||
If those ARM64 dependencies are not available, run S2VNA and the acquisition
|
If those ARM64 dependencies are not available, run S2VNA and the acquisition
|
||||||
process on an Ubuntu x86_64 machine. Raspberry Pi integration should then be
|
server on an Ubuntu x86_64 machine and use the remote K209 mode documented
|
||||||
handled at the system/pipeline level, not by replacing the K209 driver transport
|
above. In that mode, Raspberry Pi runs the project pipeline and GPIO switch
|
||||||
with a fallback.
|
drivers, while the x86_64 machine runs S2VNA and the K209 remote server.
|
||||||
|
|
||||||
## Expected Hardware Test Result
|
## Expected Hardware Test Result
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,213 @@
|
|||||||
|
# Operation Modes
|
||||||
|
|
||||||
|
The active radar backend is selected manually in JSON by `radar.model`.
|
||||||
|
The GUI does not expose a model selector.
|
||||||
|
|
||||||
|
Available models:
|
||||||
|
|
||||||
|
```text
|
||||||
|
librevna
|
||||||
|
librevna_multi
|
||||||
|
compact_m_k209
|
||||||
|
```
|
||||||
|
|
||||||
|
Example configs in the repository root:
|
||||||
|
|
||||||
|
```text
|
||||||
|
run_config_librevna.example.json
|
||||||
|
run_config_librevna_multi.example.json
|
||||||
|
run_config_compact_m_k209.example.json
|
||||||
|
run_config_compact_m_k209_local_mock_switches.example.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Commands
|
||||||
|
|
||||||
|
Build native binaries:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /path/to/radar_system
|
||||||
|
make
|
||||||
|
```
|
||||||
|
|
||||||
|
Run the GUI:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
.venv/bin/python -m python_app.gui.main
|
||||||
|
```
|
||||||
|
|
||||||
|
Run a single acquisition producer manually:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
build/bin/sweep_orchestrator --config run_config.json
|
||||||
|
```
|
||||||
|
|
||||||
|
The GUI process supervisor starts the correct producer automatically:
|
||||||
|
|
||||||
|
- `librevna` -> `build/bin/sweep_orchestrator`
|
||||||
|
- `compact_m_k209` -> `build/bin/sweep_orchestrator`
|
||||||
|
- `librevna_multi` -> `python_app.scripts.multi_device_raw_producer`
|
||||||
|
|
||||||
|
## Single LibreVNA
|
||||||
|
|
||||||
|
Use this mode when one LibreVNA is connected directly over USB to the machine
|
||||||
|
running the project.
|
||||||
|
|
||||||
|
Config:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"radar": {
|
||||||
|
"model": "librevna",
|
||||||
|
"serial": "",
|
||||||
|
"driver_mode": "native"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
|
||||||
|
- Empty `serial` means use the first compatible LibreVNA found.
|
||||||
|
- Set `serial` when multiple LibreVNAs are connected.
|
||||||
|
- `driver_mode: "native"` uses the direct USB LibreVNA driver.
|
||||||
|
- `driver_mode: "mock"` generates synthetic radar data for UI/development.
|
||||||
|
- Switch GPIO is controlled by the same machine unless switch `driver_mode` is
|
||||||
|
set to `mock`.
|
||||||
|
|
||||||
|
Typical local check without GPIO:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp run_config_librevna.example.json /tmp/librevna_mock_switches.json
|
||||||
|
# edit both switches to driver_mode="mock" if needed
|
||||||
|
build/bin/sweep_orchestrator --config /tmp/librevna_mock_switches.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## LibreVNA Multi-Device
|
||||||
|
|
||||||
|
Use this mode for one master LibreVNA and two slave LibreVNAs. This mode does
|
||||||
|
not use physical RF switch GPIO in the acquisition producer. It exposes a fixed
|
||||||
|
virtual matrix:
|
||||||
|
|
||||||
|
```text
|
||||||
|
inputs: 0..3
|
||||||
|
outputs: 0..1
|
||||||
|
combos: 8
|
||||||
|
```
|
||||||
|
|
||||||
|
Config:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"radar": {
|
||||||
|
"model": "librevna_multi",
|
||||||
|
"serial": "MASTER_SERIAL",
|
||||||
|
"driver_mode": "native",
|
||||||
|
"multi_device": {
|
||||||
|
"slave_serials": [
|
||||||
|
"SLAVE_SERIAL_1",
|
||||||
|
"SLAVE_SERIAL_2"
|
||||||
|
],
|
||||||
|
"force_external_reference": true,
|
||||||
|
"recovery_attempts": 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
|
||||||
|
- Exactly two slave serials are required.
|
||||||
|
- `force_external_reference` configures the synchronized reference workflow.
|
||||||
|
- `recovery_attempts` controls reopen/retry attempts after native acquisition
|
||||||
|
errors.
|
||||||
|
- The Python producer is selected automatically by the GUI. Manual raw-producer
|
||||||
|
run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
.venv/bin/python -m python_app.scripts.multi_device_raw_producer \
|
||||||
|
--config run_config_librevna_multi.example.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## Compact-M K209 On The Same Computer
|
||||||
|
|
||||||
|
Use this for local development on the x86_64 computer that runs S2VNA and has
|
||||||
|
the K209 connected over USB-C. GPIO can be disabled with mock switches.
|
||||||
|
|
||||||
|
1. Start S2VNA and enable HiSLIP on port `4880`.
|
||||||
|
|
||||||
|
2. Start the local project K209 server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
.venv/bin/python -m python_app.scripts.k209_remote_server \
|
||||||
|
--host 127.0.0.1 \
|
||||||
|
--port 50209
|
||||||
|
```
|
||||||
|
|
||||||
|
3. In another terminal, smoke-test the server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
.venv/bin/python -m python_app.scripts.k209_remote_smoke_test \
|
||||||
|
--host 127.0.0.1 \
|
||||||
|
--port 50209
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Run one acquisition with mock switches:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
build/bin/sweep_orchestrator \
|
||||||
|
--config run_config_compact_m_k209_local_mock_switches.example.json
|
||||||
|
```
|
||||||
|
|
||||||
|
This mode is useful on a laptop because it avoids GPIO dependencies.
|
||||||
|
|
||||||
|
## Compact-M K209 With Raspberry Pi GPIO
|
||||||
|
|
||||||
|
Use this for the real K209 + Raspberry Pi setup:
|
||||||
|
|
||||||
|
```text
|
||||||
|
K209 --USB-C--> x86_64 computer running S2VNA
|
||||||
|
x86_64 computer --Ethernet--> Raspberry Pi 5
|
||||||
|
Raspberry Pi 5 --GPIO--> RF switches
|
||||||
|
```
|
||||||
|
|
||||||
|
On the x86_64 computer:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /path/to/radar_system
|
||||||
|
.venv/bin/python -m python_app.scripts.k209_remote_server \
|
||||||
|
--host 0.0.0.0 \
|
||||||
|
--port 50209
|
||||||
|
```
|
||||||
|
|
||||||
|
On the Raspberry Pi, set `radar.remote_host` to the Ethernet IP address of the
|
||||||
|
x86_64 computer:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"radar": {
|
||||||
|
"model": "compact_m_k209",
|
||||||
|
"remote_host": "192.168.1.10",
|
||||||
|
"remote_port": 50209,
|
||||||
|
"driver_mode": "native"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Then run the GUI or producer on the Raspberry Pi:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
.venv/bin/python -m python_app.gui.main
|
||||||
|
```
|
||||||
|
|
||||||
|
For a command-line connection check from the Raspberry Pi:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
.venv/bin/python -m python_app.scripts.k209_remote_smoke_test \
|
||||||
|
--host 192.168.1.10 \
|
||||||
|
--port 50209
|
||||||
|
```
|
||||||
|
|
||||||
|
The Raspberry Pi does not need S2VNA or NI-VISA in this remote mode.
|
||||||
|
|
||||||
|
## K209 Remote Performance
|
||||||
|
|
||||||
|
The remote K209 path keeps one persistent TCP connection open. Configuration
|
||||||
|
sends sweep settings once and receives the frequency axis once. Each sweep then
|
||||||
|
sends one command byte and receives only binary `S11` and `S21` `float32`
|
||||||
|
arrays.
|
||||||
|
|
||||||
|
Use wired Ethernet. Wi-Fi works for tests but adds jitter.
|
||||||
|
|
||||||
@@ -0,0 +1,324 @@
|
|||||||
|
# Run Config Reference
|
||||||
|
|
||||||
|
`run_config.json` is the stable runtime configuration consumed by the GUI,
|
||||||
|
Python helpers, and C++ pipeline binaries. The active file is normally
|
||||||
|
`run_config.json`; root-level `*.example.json` files are templates.
|
||||||
|
|
||||||
|
JSON does not support comments. Keep notes in docs, not inside config files.
|
||||||
|
|
||||||
|
## Top-Level Sections
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"radar": {},
|
||||||
|
"switches": {},
|
||||||
|
"run": {},
|
||||||
|
"preprocess": {},
|
||||||
|
"gpr": {},
|
||||||
|
"rings": {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## `radar`
|
||||||
|
|
||||||
|
Selects the radar model and sweep settings.
|
||||||
|
|
||||||
|
```json
|
||||||
|
"radar": {
|
||||||
|
"model": "compact_m_k209",
|
||||||
|
"serial": "",
|
||||||
|
"remote_host": "127.0.0.1",
|
||||||
|
"remote_port": 50209,
|
||||||
|
"driver_mode": "native",
|
||||||
|
"mock_signal_hz": 5000000.0,
|
||||||
|
"multi_device": {},
|
||||||
|
"sweep": {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Fields:
|
||||||
|
|
||||||
|
| Field | Meaning |
|
||||||
|
| --- | --- |
|
||||||
|
| `model` | `librevna`, `librevna_multi`, or `compact_m_k209`. |
|
||||||
|
| `serial` | LibreVNA serial. Empty means first device for single LibreVNA. For `librevna_multi`, this is the master serial. |
|
||||||
|
| `remote_host` | K209 remote server host. Used by `compact_m_k209`; ignored by LibreVNA modes. |
|
||||||
|
| `remote_port` | K209 remote server TCP port. Default is `50209`. |
|
||||||
|
| `driver_mode` | `native` for hardware, `mock` for supported synthetic LibreVNA modes. K209 requires `native`. |
|
||||||
|
| `mock_signal_hz` | Existing LibreVNA mock signal parameter used by C++ mock acquisition. |
|
||||||
|
| `multi_device` | Extra settings for `librevna_multi`. |
|
||||||
|
| `sweep` | Frequency, point count, IFBW, and power settings. |
|
||||||
|
|
||||||
|
### `radar.sweep`
|
||||||
|
|
||||||
|
```json
|
||||||
|
"sweep": {
|
||||||
|
"start_hz": 1000000.0,
|
||||||
|
"stop_hz": 6000000000.0,
|
||||||
|
"points": 201,
|
||||||
|
"if_bandwidth_hz": 50000.0,
|
||||||
|
"stimulus_power_dbm": -10.0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Fields:
|
||||||
|
|
||||||
|
| Field | Meaning |
|
||||||
|
| --- | --- |
|
||||||
|
| `start_hz` | Sweep start frequency in Hz. |
|
||||||
|
| `stop_hz` | Sweep stop frequency in Hz. Must be `>= start_hz`. |
|
||||||
|
| `points` | Number of frequency points. |
|
||||||
|
| `if_bandwidth_hz` | IF bandwidth in Hz. |
|
||||||
|
| `stimulus_power_dbm` | Output power in dBm. |
|
||||||
|
|
||||||
|
K209 limits reported by the tested device:
|
||||||
|
|
||||||
|
```text
|
||||||
|
frequency_hz: 9000 .. 9000000000
|
||||||
|
ifbw_hz: 1 .. 300000
|
||||||
|
power_dbm: -55 .. +5
|
||||||
|
points: 2 .. 500001
|
||||||
|
```
|
||||||
|
|
||||||
|
### `radar.multi_device`
|
||||||
|
|
||||||
|
Used only when `radar.model == "librevna_multi"`.
|
||||||
|
|
||||||
|
```json
|
||||||
|
"multi_device": {
|
||||||
|
"slave_serials": [
|
||||||
|
"SLAVE_SERIAL_1",
|
||||||
|
"SLAVE_SERIAL_2"
|
||||||
|
],
|
||||||
|
"force_external_reference": true,
|
||||||
|
"recovery_attempts": 3
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Fields:
|
||||||
|
|
||||||
|
| Field | Meaning |
|
||||||
|
| --- | --- |
|
||||||
|
| `slave_serials` | Exactly two slave LibreVNA serials. |
|
||||||
|
| `force_external_reference` | Configure the synchronized external reference path. |
|
||||||
|
| `recovery_attempts` | Reopen/retry attempts after native multi-device acquisition errors. |
|
||||||
|
|
||||||
|
## `switches`
|
||||||
|
|
||||||
|
Two RF switch sections are used:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"switches": {
|
||||||
|
"port1": {},
|
||||||
|
"port2": {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
By convention in the C++ pipeline:
|
||||||
|
|
||||||
|
```text
|
||||||
|
port1 -> output switch
|
||||||
|
port2 -> input switch
|
||||||
|
```
|
||||||
|
|
||||||
|
Switch fields:
|
||||||
|
|
||||||
|
| Field | Meaning |
|
||||||
|
| --- | --- |
|
||||||
|
| `name` | Human-readable switch name. |
|
||||||
|
| `driver_mode` | `native` for GPIO, `mock` to avoid GPIO access. |
|
||||||
|
| `driver` | `h7992` or `hmc349a`. |
|
||||||
|
| `radar_port` | Physical radar port mapping, must be unique and either `1` or `2`. |
|
||||||
|
| `positions` | Number of switch positions. |
|
||||||
|
| `default_position` | Position selected on open. Zero-based. |
|
||||||
|
| `gpio_chip` | Linux GPIO chip path, usually `/dev/gpiochip0`. |
|
||||||
|
| `pin_a` | First GPIO control pin. |
|
||||||
|
| `pin_b` | Second GPIO control pin for `h7992`. |
|
||||||
|
| `invert_logic` | Logic inversion for supported switch drivers. |
|
||||||
|
|
||||||
|
Use mock switches on a laptop without GPIO:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"driver_mode": "mock"
|
||||||
|
```
|
||||||
|
|
||||||
|
## `run`
|
||||||
|
|
||||||
|
Runtime behavior and combo selection.
|
||||||
|
|
||||||
|
```json
|
||||||
|
"run": {
|
||||||
|
"settling_ms": 0,
|
||||||
|
"idle_sleep_ms": 2,
|
||||||
|
"continuous": true,
|
||||||
|
"processing_live_config_path": "python_app/runtime/processing_live.json",
|
||||||
|
"locator_server": {},
|
||||||
|
"combos": [
|
||||||
|
{"input": 0, "output": 0}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Fields:
|
||||||
|
|
||||||
|
| Field | Meaning |
|
||||||
|
| --- | --- |
|
||||||
|
| `settling_ms` | Delay after switching before measuring. |
|
||||||
|
| `idle_sleep_ms` | Sleep between continuous collections. |
|
||||||
|
| `continuous` | `true` loops until stopped; `false` captures one collection and exits. |
|
||||||
|
| `processing_live_config_path` | Runtime path used by processing live settings. |
|
||||||
|
| `locator_server` | Embedded TCP server settings for publishing locator results. |
|
||||||
|
| `combos` | Zero-based switch combinations to acquire. |
|
||||||
|
|
||||||
|
`combos` entries use input/output switch positions:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"input": 2, "output": 1}
|
||||||
|
```
|
||||||
|
|
||||||
|
For `librevna_multi`, the model constraints force the canonical virtual matrix:
|
||||||
|
|
||||||
|
```text
|
||||||
|
input: 0..3
|
||||||
|
output: 0..1
|
||||||
|
```
|
||||||
|
|
||||||
|
## `run.locator_server`
|
||||||
|
|
||||||
|
Settings for the embedded locator result TCP server.
|
||||||
|
|
||||||
|
| Field | Meaning |
|
||||||
|
| --- | --- |
|
||||||
|
| `device_id` | Device identifier in locator payloads. |
|
||||||
|
| `protocol_version` | Locator payload protocol version. |
|
||||||
|
| `host` | Bind host, commonly `0.0.0.0`. |
|
||||||
|
| `port` | TCP port, commonly `8888`. |
|
||||||
|
| `max_payload_bytes` | Maximum result payload size. |
|
||||||
|
| `client_queue_size` | Per-client queue size. |
|
||||||
|
| `logger_name` | Logger name used by the service. |
|
||||||
|
|
||||||
|
## `preprocess`
|
||||||
|
|
||||||
|
Names or bundle paths for calibration/reference assets used by preprocessing.
|
||||||
|
|
||||||
|
```json
|
||||||
|
"preprocess": {
|
||||||
|
"s21": {
|
||||||
|
"calibration": {"set_name": "", "bundle_path": ""},
|
||||||
|
"reference": {"set_name": "", "bundle_path": ""}
|
||||||
|
},
|
||||||
|
"s11": {
|
||||||
|
"calibration": {
|
||||||
|
"open": {"set_name": "", "bundle_path": ""},
|
||||||
|
"short": {"set_name": "", "bundle_path": ""},
|
||||||
|
"load": {"set_name": "", "bundle_path": ""}
|
||||||
|
},
|
||||||
|
"reference": {"set_name": "", "bundle_path": ""}
|
||||||
|
},
|
||||||
|
"notch": {
|
||||||
|
"enabled": true,
|
||||||
|
"bands_hz": [],
|
||||||
|
"taper_width_hz": 40000000.0,
|
||||||
|
"taper_type": "cosine"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`set_name` selects a stored set for the active radar key. `bundle_path` can
|
||||||
|
point to an exported bundle. Empty values mean no asset is selected.
|
||||||
|
|
||||||
|
`notch.bands_hz` is a list of `[low_hz, high_hz]` ranges. `taper_type` is
|
||||||
|
`cosine` or `hard`.
|
||||||
|
|
||||||
|
## `gpr`
|
||||||
|
|
||||||
|
GPR geometry and processing configuration.
|
||||||
|
|
||||||
|
```json
|
||||||
|
"gpr": {
|
||||||
|
"mode": "point",
|
||||||
|
"relative_permittivity": 1.0,
|
||||||
|
"tx_geometry": [
|
||||||
|
{"output_pos": 0, "x_m": 0.905}
|
||||||
|
],
|
||||||
|
"rx_geometry": [
|
||||||
|
{"input_pos": 0, "x_m": -0.18}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Fields:
|
||||||
|
|
||||||
|
| Field | Meaning |
|
||||||
|
| --- | --- |
|
||||||
|
| `mode` | GPR processing mode. |
|
||||||
|
| `relative_permittivity` | Medium relative permittivity used for propagation speed. |
|
||||||
|
| `tx_geometry` | Transmitter positions keyed by output switch position. |
|
||||||
|
| `rx_geometry` | Receiver positions keyed by input switch position. |
|
||||||
|
|
||||||
|
Geometry positions must match configured switch positions. For example, an
|
||||||
|
`output_pos` of `1` requires the output switch to have at least 2 positions.
|
||||||
|
|
||||||
|
## `rings`
|
||||||
|
|
||||||
|
Shared-memory ring endpoints used by native processes.
|
||||||
|
|
||||||
|
```json
|
||||||
|
"rings": {
|
||||||
|
"raw": {"name": "/radar_raw", "capacity": 50, "slot_size_bytes": 2097152},
|
||||||
|
"raw_tap": {"name": "/radar_raw_tap", "capacity": 50, "slot_size_bytes": 2097152},
|
||||||
|
"preprocessed": {"name": "/radar_preprocessed", "capacity": 50, "slot_size_bytes": 2097152},
|
||||||
|
"preprocessed_tap": {"name": "/radar_preprocessed_tap", "capacity": 50, "slot_size_bytes": 2097152},
|
||||||
|
"results": {"name": "/radar_results", "capacity": 50, "slot_size_bytes": 2097152}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Fields:
|
||||||
|
|
||||||
|
| Field | Meaning |
|
||||||
|
| --- | --- |
|
||||||
|
| `name` | POSIX shared-memory object name. |
|
||||||
|
| `capacity` | Number of slots. |
|
||||||
|
| `slot_size_bytes` | Maximum serialized payload size per slot. |
|
||||||
|
|
||||||
|
Use unique ring names for parallel tests to avoid collisions with a running GUI
|
||||||
|
session.
|
||||||
|
|
||||||
|
## Minimal Model Examples
|
||||||
|
|
||||||
|
Single LibreVNA:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"radar": {
|
||||||
|
"model": "librevna",
|
||||||
|
"serial": "",
|
||||||
|
"driver_mode": "native"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Multi-device LibreVNA:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"radar": {
|
||||||
|
"model": "librevna_multi",
|
||||||
|
"serial": "MASTER_SERIAL",
|
||||||
|
"driver_mode": "native",
|
||||||
|
"multi_device": {
|
||||||
|
"slave_serials": ["SLAVE_1", "SLAVE_2"],
|
||||||
|
"force_external_reference": true,
|
||||||
|
"recovery_attempts": 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Compact-M K209 via remote server:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"radar": {
|
||||||
|
"model": "compact_m_k209",
|
||||||
|
"remote_host": "192.168.1.10",
|
||||||
|
"remote_port": 50209,
|
||||||
|
"driver_mode": "native"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
@@ -3,10 +3,11 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from python_app.hardware_full.librevna_service import LibreVnaService
|
from python_app.hardware_full.librevna_service import LibreVnaService
|
||||||
|
from python_app.hardware_full.single_radar_service import create_single_radar_service
|
||||||
|
|
||||||
|
|
||||||
class AppWindowRadarLimitsMixin:
|
class AppWindowRadarLimitsMixin:
|
||||||
"""Handle LibreVNA capability probing and dependent UI clamping."""
|
"""Handle radar capability probing and dependent UI clamping."""
|
||||||
|
|
||||||
def _on_radar_sweep_limits_changed(self) -> None:
|
def _on_radar_sweep_limits_changed(self) -> None:
|
||||||
"""Clamp processing frequency bounds after sweep start/stop edits."""
|
"""Clamp processing frequency bounds after sweep start/stop edits."""
|
||||||
@@ -15,27 +16,19 @@ class AppWindowRadarLimitsMixin:
|
|||||||
self._on_processing_live_settings_changed()
|
self._on_processing_live_settings_changed()
|
||||||
|
|
||||||
def _refresh_radar_limits_from_device(self) -> bool:
|
def _refresh_radar_limits_from_device(self) -> bool:
|
||||||
"""Query native LibreVNA limits and apply them to GUI fields."""
|
"""Query native radar limits and apply them to GUI fields."""
|
||||||
serial = self._defaults_config.radar.serial
|
config = self._defaults_config
|
||||||
radar_service = LibreVnaService(serial=serial or None)
|
if config.is_multi_device:
|
||||||
if not radar_service.driver_available:
|
radar_service = LibreVnaService(serial=config.radar.serial or None)
|
||||||
self._fallback_to_mock_mode("LibreVNA Python driver is not available for device limits query")
|
else:
|
||||||
return False
|
radar_service = create_single_radar_service(config)
|
||||||
|
|
||||||
try:
|
if isinstance(radar_service, LibreVnaService) and not radar_service.driver_available:
|
||||||
limits = radar_service.read_device_limits()
|
raise RuntimeError("LibreVNA Python driver is not available for device limits query")
|
||||||
except Exception as exc: # noqa: BLE001
|
|
||||||
self._log_exception("Failed to query LibreVNA limits; using UI fallback", exc, level="WARN")
|
|
||||||
self._apply_radar_limits_to_ui(None)
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
limits = radar_service.read_device_limits()
|
||||||
return self._apply_radar_limits_to_ui(limits)
|
return self._apply_radar_limits_to_ui(limits)
|
||||||
|
|
||||||
def _fallback_to_mock_mode(self, reason: str) -> None:
|
|
||||||
"""Handle unavailable native limits without mutating JSON-backed mode."""
|
|
||||||
self._log_warning(reason)
|
|
||||||
self._apply_radar_limits_to_ui(None)
|
|
||||||
|
|
||||||
def _apply_radar_limits_to_ui(self, limits: dict[str, float | int] | None) -> bool:
|
def _apply_radar_limits_to_ui(self, limits: dict[str, float | int] | None) -> bool:
|
||||||
"""Apply optional radar limits and clamp dependent GUI fields."""
|
"""Apply optional radar limits and clamp dependent GUI fields."""
|
||||||
previous_limits = dict(self._radar_limits) if self._radar_limits is not None else None
|
previous_limits = dict(self._radar_limits) if self._radar_limits is not None else None
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import time
|
|||||||
|
|
||||||
from python_app.gui.runtime.constraints import validate_processing_mode_constraints
|
from python_app.gui.runtime.constraints import validate_processing_mode_constraints
|
||||||
from python_app.gui.runtime.history import build_run_history_signature, record_result_history
|
from python_app.gui.runtime.history import build_run_history_signature, record_result_history
|
||||||
from python_app.hardware_full.librevna_service import LibreVnaService
|
from python_app.hardware_full.single_radar_service import create_single_radar_service
|
||||||
from python_app.models.dataset_model import ComboKey, ResultCollection, SweepCollection
|
from python_app.models.dataset_model import ComboKey, ResultCollection, SweepCollection
|
||||||
from python_app.models.run_config_model import RunConfigModel
|
from python_app.models.run_config_model import RunConfigModel
|
||||||
from python_app.orchestration.gpr_locator import collection_has_gpr_payloads
|
from python_app.orchestration.gpr_locator import collection_has_gpr_payloads
|
||||||
@@ -181,7 +181,9 @@ class AppWindowPipelineMixin:
|
|||||||
self._start_run()
|
self._start_run()
|
||||||
|
|
||||||
def _prepare_radar_for_native_acquisition(self, config: RunConfigModel) -> None:
|
def _prepare_radar_for_native_acquisition(self, config: RunConfigModel) -> None:
|
||||||
"""Preconfigure native LibreVNA using current sweep settings."""
|
"""Preconfigure native single-radar hardware using current sweep settings."""
|
||||||
|
if config.radar.model == RunConfigModel.COMPACT_M_K209_MODEL and config.radar.driver_mode != "native":
|
||||||
|
raise RuntimeError("Compact-M K209 requires radar.driver_mode='native'")
|
||||||
if config.radar.driver_mode != "native":
|
if config.radar.driver_mode != "native":
|
||||||
self._log("Radar pre-configuration skipped (mock mode)")
|
self._log("Radar pre-configuration skipped (mock mode)")
|
||||||
return
|
return
|
||||||
@@ -189,8 +191,8 @@ class AppWindowPipelineMixin:
|
|||||||
self._log("Multi-device raw producer will configure all LibreVNA devices")
|
self._log("Multi-device raw producer will configure all LibreVNA devices")
|
||||||
return
|
return
|
||||||
|
|
||||||
radar_service = LibreVnaService(serial=config.radar.serial or None)
|
radar_service = create_single_radar_service(config)
|
||||||
if not radar_service.driver_available:
|
if not getattr(radar_service, "driver_available", True):
|
||||||
raise RuntimeError("LibreVNA Python driver is not available for native pre-configuration")
|
raise RuntimeError("LibreVNA Python driver is not available for native pre-configuration")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -199,7 +201,7 @@ class AppWindowPipelineMixin:
|
|||||||
finally:
|
finally:
|
||||||
radar_service.close()
|
radar_service.close()
|
||||||
|
|
||||||
self._log("Radar pre-configured via Python driver")
|
self._log(f"Radar pre-configured via Python driver: model={config.radar.model}")
|
||||||
|
|
||||||
def _stop_run(self) -> None:
|
def _stop_run(self) -> None:
|
||||||
"""Stop acquisition-side processes and close readers as needed."""
|
"""Stop acquisition-side processes and close readers as needed."""
|
||||||
|
|||||||
@@ -110,16 +110,29 @@ class CompactMK209Service:
|
|||||||
|
|
||||||
def read_device_limits(self) -> dict[str, float | int]:
|
def read_device_limits(self) -> dict[str, float | int]:
|
||||||
"""Read analyzer limits through SCPI capability/service queries."""
|
"""Read analyzer limits through SCPI capability/service queries."""
|
||||||
instrument = self._require_instrument()
|
opened_here = self._instrument is None
|
||||||
return {
|
try:
|
||||||
"min_frequency_hz": float(instrument.query("SERV:SWE:FREQ:MIN?")),
|
if opened_here:
|
||||||
"max_frequency_hz": float(instrument.query("SERV:SWE:FREQ:MAX?")),
|
self.open()
|
||||||
"min_ifbw_hz": float(instrument.query("SYST:CAP:IFBW:MIN?")),
|
instrument = self._require_instrument()
|
||||||
"max_ifbw_hz": float(instrument.query("SYST:CAP:IFBW:MAX?")),
|
return {
|
||||||
"max_points": int(float(instrument.query("SERV:SWE:POIN?"))),
|
"min_frequency_hz": float(instrument.query("SERV:SWE:FREQ:MIN?")),
|
||||||
"min_power_dbm": float(instrument.query("SERV:SWE:POW:MIN?")),
|
"max_frequency_hz": float(instrument.query("SERV:SWE:FREQ:MAX?")),
|
||||||
"max_power_dbm": float(instrument.query("SERV:SWE:POW:MAX?")),
|
"min_ifbw_hz": float(instrument.query("SYST:CAP:IFBW:MIN?")),
|
||||||
}
|
"max_ifbw_hz": float(instrument.query("SYST:CAP:IFBW:MAX?")),
|
||||||
|
"max_points": int(float(instrument.query("SERV:SWE:POIN?"))),
|
||||||
|
"min_power_dbm": float(instrument.query("SERV:SWE:POW:MIN?")),
|
||||||
|
"max_power_dbm": float(instrument.query("SERV:SWE:POW:MAX?")),
|
||||||
|
}
|
||||||
|
finally:
|
||||||
|
if opened_here:
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
def frequency_axis(self) -> np.ndarray:
|
||||||
|
"""Return the configured frequency axis."""
|
||||||
|
if self._frequency_hz is None:
|
||||||
|
raise RuntimeError("K209 frequency axis is not configured")
|
||||||
|
return self._frequency_hz
|
||||||
|
|
||||||
def acquire_interleaved(self) -> CompactMK209InterleavedSweep:
|
def acquire_interleaved(self) -> CompactMK209InterleavedSweep:
|
||||||
"""Acquire one corrected sweep without converting interleaved arrays."""
|
"""Acquire one corrected sweep without converting interleaved arrays."""
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
"""Binary TCP protocol shared by the K209 remote server and Python client."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import socket
|
||||||
|
import struct
|
||||||
|
from typing import BinaryIO
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
COMMAND_IDENTITY = b"I"
|
||||||
|
COMMAND_LIMITS = b"L"
|
||||||
|
COMMAND_CONFIGURE = b"C"
|
||||||
|
COMMAND_ACQUIRE = b"A"
|
||||||
|
|
||||||
|
STATUS_OK = b"O"
|
||||||
|
STATUS_ERROR = b"E"
|
||||||
|
|
||||||
|
DEFAULT_REMOTE_HOST = "127.0.0.1"
|
||||||
|
DEFAULT_REMOTE_PORT = 50209
|
||||||
|
|
||||||
|
CONFIG_STRUCT = struct.Struct("!ddIdd")
|
||||||
|
LIMITS_STRUCT = struct.Struct("!ddddIdd")
|
||||||
|
U32_STRUCT = struct.Struct("!I")
|
||||||
|
|
||||||
|
|
||||||
|
def recv_exact(stream: socket.socket | BinaryIO, size: int) -> bytes:
|
||||||
|
"""Read exactly `size` bytes from a socket-like object."""
|
||||||
|
chunks: list[bytes] = []
|
||||||
|
remaining = int(size)
|
||||||
|
while remaining > 0:
|
||||||
|
chunk = stream.recv(remaining) if isinstance(stream, socket.socket) else stream.read(remaining)
|
||||||
|
if not chunk:
|
||||||
|
raise ConnectionError(f"K209 remote connection closed with {remaining} bytes pending")
|
||||||
|
chunks.append(chunk)
|
||||||
|
remaining -= len(chunk)
|
||||||
|
return b"".join(chunks)
|
||||||
|
|
||||||
|
|
||||||
|
def send_all(stream: socket.socket | BinaryIO, payload: bytes) -> None:
|
||||||
|
"""Write all payload bytes to a socket-like object."""
|
||||||
|
if isinstance(stream, socket.socket):
|
||||||
|
stream.sendall(payload)
|
||||||
|
return
|
||||||
|
stream.write(payload)
|
||||||
|
|
||||||
|
|
||||||
|
def send_u32(stream: socket.socket | BinaryIO, value: int) -> None:
|
||||||
|
"""Send one network-order uint32."""
|
||||||
|
send_all(stream, U32_STRUCT.pack(int(value)))
|
||||||
|
|
||||||
|
|
||||||
|
def recv_u32(stream: socket.socket | BinaryIO) -> int:
|
||||||
|
"""Read one network-order uint32."""
|
||||||
|
return int(U32_STRUCT.unpack(recv_exact(stream, U32_STRUCT.size))[0])
|
||||||
|
|
||||||
|
|
||||||
|
def send_error(stream: socket.socket | BinaryIO, message: str) -> None:
|
||||||
|
"""Send protocol error response."""
|
||||||
|
payload = str(message).encode("utf-8", errors="replace")
|
||||||
|
send_all(stream, STATUS_ERROR)
|
||||||
|
send_u32(stream, len(payload))
|
||||||
|
send_all(stream, payload)
|
||||||
|
|
||||||
|
|
||||||
|
def read_status(stream: socket.socket | BinaryIO) -> None:
|
||||||
|
"""Read response status and raise remote error when needed."""
|
||||||
|
status = recv_exact(stream, 1)
|
||||||
|
if status == STATUS_OK:
|
||||||
|
return
|
||||||
|
if status == STATUS_ERROR:
|
||||||
|
message = recv_exact(stream, recv_u32(stream)).decode("utf-8", errors="replace")
|
||||||
|
raise RuntimeError(f"K209 remote server error: {message}")
|
||||||
|
raise RuntimeError(f"K209 remote server returned invalid status byte: {status!r}")
|
||||||
|
|
||||||
|
|
||||||
|
def send_float32_array(stream: socket.socket | BinaryIO, values: np.ndarray) -> None:
|
||||||
|
"""Send a float32 array as a length-prefixed little-endian payload."""
|
||||||
|
payload = np.asarray(values, dtype="<f4").tobytes(order="C")
|
||||||
|
send_u32(stream, len(payload))
|
||||||
|
send_all(stream, payload)
|
||||||
|
|
||||||
|
|
||||||
|
def recv_float32_array(stream: socket.socket | BinaryIO, expected_values: int) -> np.ndarray:
|
||||||
|
"""Read a length-prefixed little-endian float32 array."""
|
||||||
|
payload_size = recv_u32(stream)
|
||||||
|
expected_size = int(expected_values) * np.dtype(np.float32).itemsize
|
||||||
|
if payload_size != expected_size:
|
||||||
|
raise RuntimeError(f"K209 remote payload has {payload_size} bytes, expected {expected_size}")
|
||||||
|
return np.frombuffer(recv_exact(stream, payload_size), dtype="<f4").copy()
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
"""TCP client for a Compact-M K209 connected to a remote S2VNA host."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
import socket
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from python_app.hardware_full.k209_remote_protocol import (
|
||||||
|
COMMAND_ACQUIRE,
|
||||||
|
COMMAND_CONFIGURE,
|
||||||
|
COMMAND_IDENTITY,
|
||||||
|
COMMAND_LIMITS,
|
||||||
|
CONFIG_STRUCT,
|
||||||
|
DEFAULT_REMOTE_HOST,
|
||||||
|
DEFAULT_REMOTE_PORT,
|
||||||
|
LIMITS_STRUCT,
|
||||||
|
read_status,
|
||||||
|
recv_exact,
|
||||||
|
recv_float32_array,
|
||||||
|
recv_u32,
|
||||||
|
)
|
||||||
|
from python_app.hardware_full.librevna_driver.models import SweepResult
|
||||||
|
from python_app.models.run_config_model import RadarSweepModel
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True)
|
||||||
|
class RemoteCompactMK209Service:
|
||||||
|
"""Acquire K209 sweeps through a persistent TCP connection."""
|
||||||
|
|
||||||
|
host: str = DEFAULT_REMOTE_HOST
|
||||||
|
port: int = DEFAULT_REMOTE_PORT
|
||||||
|
timeout_s: float = 20.0
|
||||||
|
_socket: socket.socket | None = field(init=False, default=None, repr=False)
|
||||||
|
_settings: RadarSweepModel | None = field(init=False, default=None, repr=False)
|
||||||
|
_frequency_hz: np.ndarray | None = field(init=False, default=None, repr=False)
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
self.host = str(self.host).strip()
|
||||||
|
if not self.host:
|
||||||
|
raise ValueError("K209 remote host must not be empty")
|
||||||
|
self.port = int(self.port)
|
||||||
|
if self.port <= 0 or self.port > 65535:
|
||||||
|
raise ValueError("K209 remote port must be in 1..65535")
|
||||||
|
self.timeout_s = float(self.timeout_s)
|
||||||
|
if self.timeout_s <= 0.0:
|
||||||
|
raise ValueError("K209 remote timeout_s must be > 0")
|
||||||
|
|
||||||
|
def open(self) -> None:
|
||||||
|
"""Open TCP connection and apply stored settings when present."""
|
||||||
|
if self._socket is not None:
|
||||||
|
return
|
||||||
|
sock = socket.create_connection((self.host, self.port), timeout=self.timeout_s)
|
||||||
|
sock.settimeout(self.timeout_s)
|
||||||
|
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
||||||
|
self._socket = sock
|
||||||
|
try:
|
||||||
|
if self._settings is not None:
|
||||||
|
self._apply_configuration(self._settings)
|
||||||
|
except Exception:
|
||||||
|
self.close()
|
||||||
|
raise
|
||||||
|
|
||||||
|
def close(self) -> None:
|
||||||
|
"""Close TCP connection."""
|
||||||
|
if self._socket is None:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
self._socket.close()
|
||||||
|
finally:
|
||||||
|
self._socket = None
|
||||||
|
|
||||||
|
def query_identity(self) -> str:
|
||||||
|
"""Read analyzer identity string through the remote server."""
|
||||||
|
sock = self._require_socket()
|
||||||
|
sock.sendall(COMMAND_IDENTITY)
|
||||||
|
read_status(sock)
|
||||||
|
return recv_exact(sock, recv_u32(sock)).decode("utf-8", errors="replace").strip()
|
||||||
|
|
||||||
|
def read_device_limits(self) -> dict[str, float | int]:
|
||||||
|
"""Read analyzer limits through the remote server."""
|
||||||
|
opened_here = self._socket is None
|
||||||
|
try:
|
||||||
|
if opened_here:
|
||||||
|
self.open()
|
||||||
|
sock = self._require_socket()
|
||||||
|
sock.sendall(COMMAND_LIMITS)
|
||||||
|
read_status(sock)
|
||||||
|
min_freq, max_freq, min_ifbw, max_ifbw, max_points, min_power, max_power = LIMITS_STRUCT.unpack(
|
||||||
|
recv_exact(sock, LIMITS_STRUCT.size)
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"min_frequency_hz": float(min_freq),
|
||||||
|
"max_frequency_hz": float(max_freq),
|
||||||
|
"min_ifbw_hz": float(min_ifbw),
|
||||||
|
"max_ifbw_hz": float(max_ifbw),
|
||||||
|
"max_points": int(max_points),
|
||||||
|
"min_power_dbm": float(min_power),
|
||||||
|
"max_power_dbm": float(max_power),
|
||||||
|
}
|
||||||
|
finally:
|
||||||
|
if opened_here:
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
def configure(self, sweep: RadarSweepModel) -> None:
|
||||||
|
"""Store and apply sweep settings."""
|
||||||
|
self._validate_sweep(sweep)
|
||||||
|
self._settings = sweep
|
||||||
|
self._frequency_hz = None
|
||||||
|
if self._socket is not None:
|
||||||
|
self._apply_configuration(sweep)
|
||||||
|
|
||||||
|
def acquire(self) -> SweepResult:
|
||||||
|
"""Acquire one corrected S11/S21 sweep."""
|
||||||
|
if self._settings is None or self._frequency_hz is None:
|
||||||
|
raise RuntimeError("K209 remote service is not configured")
|
||||||
|
sock = self._require_socket()
|
||||||
|
points = int(self._settings.points)
|
||||||
|
sock.sendall(COMMAND_ACQUIRE)
|
||||||
|
read_status(sock)
|
||||||
|
returned_points = recv_u32(sock)
|
||||||
|
if returned_points != points:
|
||||||
|
raise RuntimeError(f"K209 remote sweep returned {returned_points} points, expected {points}")
|
||||||
|
s11_values = recv_float32_array(sock, points * 2)
|
||||||
|
s21_values = recv_float32_array(sock, points * 2)
|
||||||
|
return SweepResult(
|
||||||
|
x=self._frequency_hz.copy(),
|
||||||
|
traces={
|
||||||
|
"s11": self._complex_from_interleaved(s11_values),
|
||||||
|
"s21": self._complex_from_interleaved(s21_values),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
def _apply_configuration(self, sweep: RadarSweepModel) -> None:
|
||||||
|
sock = self._require_socket()
|
||||||
|
sock.sendall(COMMAND_CONFIGURE)
|
||||||
|
sock.sendall(
|
||||||
|
CONFIG_STRUCT.pack(
|
||||||
|
float(sweep.start_hz),
|
||||||
|
float(sweep.stop_hz),
|
||||||
|
int(sweep.points),
|
||||||
|
float(sweep.if_bandwidth_hz),
|
||||||
|
float(sweep.power_dbm),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
read_status(sock)
|
||||||
|
returned_points = recv_u32(sock)
|
||||||
|
if returned_points != int(sweep.points):
|
||||||
|
raise RuntimeError(f"K209 remote config returned {returned_points} points, expected {sweep.points}")
|
||||||
|
self._frequency_hz = recv_float32_array(sock, int(sweep.points))
|
||||||
|
|
||||||
|
def _require_socket(self) -> socket.socket:
|
||||||
|
if self._socket is None:
|
||||||
|
raise RuntimeError("K209 remote socket is not open")
|
||||||
|
return self._socket
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _validate_sweep(sweep: RadarSweepModel) -> None:
|
||||||
|
if int(sweep.points) < 2:
|
||||||
|
raise ValueError("K209 sweep points must be >= 2")
|
||||||
|
if float(sweep.stop_hz) < float(sweep.start_hz):
|
||||||
|
raise ValueError("K209 sweep stop_hz must be >= start_hz")
|
||||||
|
if float(sweep.if_bandwidth_hz) <= 0.0:
|
||||||
|
raise ValueError("K209 IF bandwidth must be > 0")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _complex_from_interleaved(values: np.ndarray) -> np.ndarray:
|
||||||
|
if values.size % 2 != 0:
|
||||||
|
raise RuntimeError("K209 remote complex trace payload has odd scalar count")
|
||||||
|
reshaped = np.asarray(values, dtype=np.float32).reshape((-1, 2))
|
||||||
|
return (reshaped[:, 0] + 1j * reshaped[:, 1]).astype(np.complex64)
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
"""Factory for single-radar Python acquisition services."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Protocol
|
||||||
|
|
||||||
|
from python_app.hardware_full.librevna_driver.models import SweepResult
|
||||||
|
from python_app.hardware_full.librevna_service import LibreVnaService
|
||||||
|
from python_app.hardware_full.remote_compact_m_k209_service import RemoteCompactMK209Service
|
||||||
|
from python_app.models.run_config_model import RadarSweepModel, RunConfigModel
|
||||||
|
|
||||||
|
|
||||||
|
class SingleRadarService(Protocol):
|
||||||
|
"""Common API used by single-radar workflows."""
|
||||||
|
|
||||||
|
def open(self) -> None:
|
||||||
|
"""Open radar connection."""
|
||||||
|
|
||||||
|
def close(self) -> None:
|
||||||
|
"""Close radar connection."""
|
||||||
|
|
||||||
|
def configure(self, sweep: RadarSweepModel) -> None:
|
||||||
|
"""Apply sweep settings."""
|
||||||
|
|
||||||
|
def read_device_limits(self) -> dict[str, float | int]:
|
||||||
|
"""Read device capability limits."""
|
||||||
|
|
||||||
|
def acquire(self) -> SweepResult:
|
||||||
|
"""Acquire one sweep."""
|
||||||
|
|
||||||
|
|
||||||
|
def create_single_radar_service(config: RunConfigModel) -> SingleRadarService:
|
||||||
|
"""Create the Python service for a non-multi-device radar config."""
|
||||||
|
if config.is_multi_device:
|
||||||
|
raise RuntimeError("single-radar service factory does not support librevna_multi")
|
||||||
|
|
||||||
|
model = config.radar.model or RunConfigModel.LIBREVNA_MODEL
|
||||||
|
if model == RunConfigModel.LIBREVNA_MODEL:
|
||||||
|
return LibreVnaService(serial=config.radar.serial or None)
|
||||||
|
|
||||||
|
if model == RunConfigModel.COMPACT_M_K209_MODEL:
|
||||||
|
if config.radar.driver_mode != "native":
|
||||||
|
raise RuntimeError("Compact-M K209 requires radar.driver_mode='native'")
|
||||||
|
return RemoteCompactMK209Service(
|
||||||
|
host=config.radar.remote_host,
|
||||||
|
port=config.radar.remote_port,
|
||||||
|
)
|
||||||
|
|
||||||
|
raise RuntimeError(f"Unsupported single-radar model: {model}")
|
||||||
@@ -57,6 +57,8 @@ def run_config_from_dict(payload: dict[str, Any]) -> RunConfigModel:
|
|||||||
|
|
||||||
model.radar.model = str(radar_payload.get("model", model.radar.model))
|
model.radar.model = str(radar_payload.get("model", model.radar.model))
|
||||||
model.radar.serial = str(radar_payload.get("serial", model.radar.serial))
|
model.radar.serial = str(radar_payload.get("serial", model.radar.serial))
|
||||||
|
model.radar.remote_host = str(radar_payload.get("remote_host", model.radar.remote_host))
|
||||||
|
model.radar.remote_port = int(radar_payload.get("remote_port", model.radar.remote_port))
|
||||||
model.radar.driver_mode = str(radar_payload.get("driver_mode", model.radar.driver_mode))
|
model.radar.driver_mode = str(radar_payload.get("driver_mode", model.radar.driver_mode))
|
||||||
model.radar.mock_signal_hz = float(radar_payload.get("mock_signal_hz", model.radar.mock_signal_hz))
|
model.radar.mock_signal_hz = float(radar_payload.get("mock_signal_hz", model.radar.mock_signal_hz))
|
||||||
|
|
||||||
@@ -239,6 +241,8 @@ def run_config_to_dict(model: RunConfigModel) -> dict[str, Any]:
|
|||||||
"radar": {
|
"radar": {
|
||||||
"model": model.radar.model,
|
"model": model.radar.model,
|
||||||
"serial": model.radar.serial,
|
"serial": model.radar.serial,
|
||||||
|
"remote_host": model.radar.remote_host,
|
||||||
|
"remote_port": model.radar.remote_port,
|
||||||
"driver_mode": model.radar.driver_mode,
|
"driver_mode": model.radar.driver_mode,
|
||||||
"mock_signal_hz": model.radar.mock_signal_hz,
|
"mock_signal_hz": model.radar.mock_signal_hz,
|
||||||
"multi_device": {
|
"multi_device": {
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ class RadarModel:
|
|||||||
|
|
||||||
model: str = "librevna"
|
model: str = "librevna"
|
||||||
serial: str = ""
|
serial: str = ""
|
||||||
|
remote_host: str = "127.0.0.1"
|
||||||
|
remote_port: int = 50209
|
||||||
driver_mode: str = "mock"
|
driver_mode: str = "mock"
|
||||||
mock_signal_hz: float = 1_000_000.0
|
mock_signal_hz: float = 1_000_000.0
|
||||||
sweep: RadarSweepModel = field(default_factory=RadarSweepModel)
|
sweep: RadarSweepModel = field(default_factory=RadarSweepModel)
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
|||||||
if str(PROJECT_ROOT) not in sys.path:
|
if str(PROJECT_ROOT) not in sys.path:
|
||||||
sys.path.insert(0, str(PROJECT_ROOT))
|
sys.path.insert(0, str(PROJECT_ROOT))
|
||||||
|
|
||||||
from python_app.hardware_full.librevna_service import LibreVnaService
|
from python_app.hardware_full.single_radar_service import create_single_radar_service
|
||||||
from python_app.models.run_config_model import RadarSweepModel
|
from python_app.models.run_config_model import RunConfigModel
|
||||||
from python_app.orchestration.shm_reader import ShmRingReader
|
from python_app.orchestration.shm_reader import ShmRingReader
|
||||||
|
|
||||||
|
|
||||||
@@ -71,22 +71,15 @@ def _read_native_summary(config_path: Path) -> str:
|
|||||||
|
|
||||||
def _prepare_radar_if_needed(config_path: Path, *, strict: bool) -> str | None:
|
def _prepare_radar_if_needed(config_path: Path, *, strict: bool) -> str | None:
|
||||||
"""Preconfigure native radar through Python service when requested."""
|
"""Preconfigure native radar through Python service when requested."""
|
||||||
config = json.loads(config_path.read_text(encoding="utf-8"))
|
config_payload = json.loads(config_path.read_text(encoding="utf-8"))
|
||||||
radar = config["radar"]
|
config = RunConfigModel.from_dict(config_payload)
|
||||||
if radar["driver_mode"] != "native":
|
if config.radar.driver_mode != "native":
|
||||||
return "Radar pre-configuration skipped (mock mode)."
|
return "Radar pre-configuration skipped (mock mode)."
|
||||||
|
if config.is_multi_device:
|
||||||
|
return "Radar pre-configuration skipped (multi-device producer config)."
|
||||||
|
|
||||||
sweep = radar["sweep"]
|
radar_service = create_single_radar_service(config)
|
||||||
sweep_model = RadarSweepModel(
|
if not getattr(radar_service, "driver_available", True):
|
||||||
start_hz=float(sweep["start_hz"]),
|
|
||||||
stop_hz=float(sweep["stop_hz"]),
|
|
||||||
points=int(sweep["points"]),
|
|
||||||
if_bandwidth_hz=float(sweep["if_bandwidth_hz"]),
|
|
||||||
power_dbm=float(sweep.get("stimulus_power_dbm", -10.0)),
|
|
||||||
)
|
|
||||||
|
|
||||||
radar_service = LibreVnaService(serial=radar.get("serial") or None)
|
|
||||||
if not radar_service.driver_available:
|
|
||||||
message = "LibreVNA Python driver is unavailable: skipping pre-configuration"
|
message = "LibreVNA Python driver is unavailable: skipping pre-configuration"
|
||||||
if strict:
|
if strict:
|
||||||
raise RuntimeError(message)
|
raise RuntimeError(message)
|
||||||
@@ -94,7 +87,7 @@ def _prepare_radar_if_needed(config_path: Path, *, strict: bool) -> str | None:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
radar_service.open()
|
radar_service.open()
|
||||||
radar_service.configure(sweep_model)
|
radar_service.configure(config.radar.sweep)
|
||||||
return "Radar pre-configuration completed."
|
return "Radar pre-configuration completed."
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
message = f"Radar pre-configuration failed ({exc})"
|
message = f"Radar pre-configuration failed ({exc})"
|
||||||
|
|||||||
@@ -0,0 +1,151 @@
|
|||||||
|
"""Run a TCP acquisition server for a locally connected Compact-M K209."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import socket
|
||||||
|
import socketserver
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
if str(PROJECT_ROOT) not in sys.path:
|
||||||
|
sys.path.insert(0, str(PROJECT_ROOT))
|
||||||
|
|
||||||
|
from python_app.hardware_full.compact_m_k209_service import CompactMK209Service
|
||||||
|
from python_app.hardware_full.k209_remote_protocol import (
|
||||||
|
COMMAND_ACQUIRE,
|
||||||
|
COMMAND_CONFIGURE,
|
||||||
|
COMMAND_IDENTITY,
|
||||||
|
COMMAND_LIMITS,
|
||||||
|
CONFIG_STRUCT,
|
||||||
|
DEFAULT_REMOTE_PORT,
|
||||||
|
LIMITS_STRUCT,
|
||||||
|
STATUS_OK,
|
||||||
|
recv_exact,
|
||||||
|
send_error,
|
||||||
|
send_float32_array,
|
||||||
|
send_u32,
|
||||||
|
)
|
||||||
|
from python_app.models.run_config_model import RadarSweepModel
|
||||||
|
|
||||||
|
DEFAULT_RESOURCE = "TCPIP0::127.0.0.1::hislip0,4880::INSTR"
|
||||||
|
|
||||||
|
|
||||||
|
class K209RemoteRequestHandler(socketserver.StreamRequestHandler):
|
||||||
|
"""Handle one persistent K209 remote client connection."""
|
||||||
|
|
||||||
|
def setup(self) -> None:
|
||||||
|
super().setup()
|
||||||
|
self.request.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
||||||
|
self.service = CompactMK209Service(
|
||||||
|
resource=self.server.resource,
|
||||||
|
timeout_ms=self.server.timeout_ms,
|
||||||
|
preset_on_open=False,
|
||||||
|
visa_library="@ivi",
|
||||||
|
)
|
||||||
|
self.service.open()
|
||||||
|
|
||||||
|
def finish(self) -> None:
|
||||||
|
try:
|
||||||
|
self.service.close()
|
||||||
|
finally:
|
||||||
|
super().finish()
|
||||||
|
|
||||||
|
def handle(self) -> None:
|
||||||
|
while True:
|
||||||
|
command = self.rfile.read(1)
|
||||||
|
if not command:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
if command == COMMAND_IDENTITY:
|
||||||
|
self._handle_identity()
|
||||||
|
elif command == COMMAND_LIMITS:
|
||||||
|
self._handle_limits()
|
||||||
|
elif command == COMMAND_CONFIGURE:
|
||||||
|
self._handle_configure()
|
||||||
|
elif command == COMMAND_ACQUIRE:
|
||||||
|
self._handle_acquire()
|
||||||
|
else:
|
||||||
|
raise RuntimeError(f"unsupported command byte {command!r}")
|
||||||
|
self.wfile.flush()
|
||||||
|
except Exception as exc: # noqa: BLE001
|
||||||
|
send_error(self.wfile, str(exc))
|
||||||
|
self.wfile.flush()
|
||||||
|
|
||||||
|
def _handle_identity(self) -> None:
|
||||||
|
payload = self.service.query_identity().encode("utf-8")
|
||||||
|
self.wfile.write(STATUS_OK)
|
||||||
|
send_u32(self.wfile, len(payload))
|
||||||
|
self.wfile.write(payload)
|
||||||
|
|
||||||
|
def _handle_limits(self) -> None:
|
||||||
|
limits = self.service.read_device_limits()
|
||||||
|
self.wfile.write(STATUS_OK)
|
||||||
|
self.wfile.write(
|
||||||
|
LIMITS_STRUCT.pack(
|
||||||
|
float(limits["min_frequency_hz"]),
|
||||||
|
float(limits["max_frequency_hz"]),
|
||||||
|
float(limits["min_ifbw_hz"]),
|
||||||
|
float(limits["max_ifbw_hz"]),
|
||||||
|
int(limits["max_points"]),
|
||||||
|
float(limits["min_power_dbm"]),
|
||||||
|
float(limits["max_power_dbm"]),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def _handle_configure(self) -> None:
|
||||||
|
start_hz, stop_hz, points, ifbw_hz, power_dbm = CONFIG_STRUCT.unpack(
|
||||||
|
recv_exact(self.rfile, CONFIG_STRUCT.size)
|
||||||
|
)
|
||||||
|
sweep = RadarSweepModel(
|
||||||
|
start_hz=start_hz,
|
||||||
|
stop_hz=stop_hz,
|
||||||
|
points=int(points),
|
||||||
|
if_bandwidth_hz=ifbw_hz,
|
||||||
|
power_dbm=power_dbm,
|
||||||
|
)
|
||||||
|
self.service.configure(sweep)
|
||||||
|
self.wfile.write(STATUS_OK)
|
||||||
|
send_u32(self.wfile, int(points))
|
||||||
|
send_float32_array(self.wfile, self.service.frequency_axis())
|
||||||
|
|
||||||
|
def _handle_acquire(self) -> None:
|
||||||
|
sweep = self.service.acquire_interleaved()
|
||||||
|
self.wfile.write(STATUS_OK)
|
||||||
|
send_u32(self.wfile, int(sweep.frequency_hz.size))
|
||||||
|
send_float32_array(self.wfile, sweep.s11_values)
|
||||||
|
send_float32_array(self.wfile, sweep.s21_values)
|
||||||
|
|
||||||
|
|
||||||
|
class K209RemoteServer(socketserver.TCPServer):
|
||||||
|
"""Single-client TCP server with K209 connection settings."""
|
||||||
|
|
||||||
|
allow_reuse_address = True
|
||||||
|
|
||||||
|
def __init__(self, server_address: tuple[str, int], resource: str, timeout_ms: int) -> None:
|
||||||
|
self.resource = resource
|
||||||
|
self.timeout_ms = timeout_ms
|
||||||
|
super().__init__(server_address, K209RemoteRequestHandler)
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_args() -> argparse.Namespace:
|
||||||
|
parser = argparse.ArgumentParser(description="Serve a locally connected Compact-M K209 over TCP.")
|
||||||
|
parser.add_argument("--host", default="0.0.0.0", help="Server bind address.")
|
||||||
|
parser.add_argument("--port", type=int, default=DEFAULT_REMOTE_PORT, help="Server TCP port.")
|
||||||
|
parser.add_argument("--resource", default=DEFAULT_RESOURCE, help="Local S2VNA VISA resource.")
|
||||||
|
parser.add_argument("--timeout-ms", type=int, default=20_000, help="K209 VISA timeout.")
|
||||||
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
args = _parse_args()
|
||||||
|
with K209RemoteServer((args.host, args.port), resource=args.resource, timeout_ms=args.timeout_ms) as server:
|
||||||
|
print(f"K209 remote server listening on {args.host}:{args.port}")
|
||||||
|
print(f"Local S2VNA resource: {args.resource}")
|
||||||
|
server.serve_forever()
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
"""Smoke test for a remote Compact-M K209 server."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
if str(PROJECT_ROOT) not in sys.path:
|
||||||
|
sys.path.insert(0, str(PROJECT_ROOT))
|
||||||
|
|
||||||
|
from python_app.hardware_full.k209_remote_protocol import DEFAULT_REMOTE_HOST, DEFAULT_REMOTE_PORT
|
||||||
|
from python_app.hardware_full.remote_compact_m_k209_service import RemoteCompactMK209Service
|
||||||
|
from python_app.models.run_config_model import RadarSweepModel
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_args() -> argparse.Namespace:
|
||||||
|
parser = argparse.ArgumentParser(description="Validate remote K209 connection and one sweep.")
|
||||||
|
parser.add_argument("--host", default=DEFAULT_REMOTE_HOST, help="K209 remote server host.")
|
||||||
|
parser.add_argument("--port", type=int, default=DEFAULT_REMOTE_PORT, help="K209 remote server port.")
|
||||||
|
parser.add_argument("--start-hz", type=float, default=10_000_000.0)
|
||||||
|
parser.add_argument("--stop-hz", type=float, default=100_000_000.0)
|
||||||
|
parser.add_argument("--points", type=int, default=11)
|
||||||
|
parser.add_argument("--ifbw-hz", type=float, default=10_000.0)
|
||||||
|
parser.add_argument("--power-dbm", type=float, default=-20.0)
|
||||||
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
args = _parse_args()
|
||||||
|
sweep = RadarSweepModel(
|
||||||
|
start_hz=args.start_hz,
|
||||||
|
stop_hz=args.stop_hz,
|
||||||
|
points=args.points,
|
||||||
|
if_bandwidth_hz=args.ifbw_hz,
|
||||||
|
power_dbm=args.power_dbm,
|
||||||
|
)
|
||||||
|
service = RemoteCompactMK209Service(host=args.host, port=args.port)
|
||||||
|
try:
|
||||||
|
service.open()
|
||||||
|
print(f"K209 IDN: {service.query_identity()}")
|
||||||
|
service.configure(sweep)
|
||||||
|
result = service.acquire()
|
||||||
|
finally:
|
||||||
|
service.close()
|
||||||
|
|
||||||
|
s11 = result.trace("s11")
|
||||||
|
s21 = result.trace("s21")
|
||||||
|
if result.x.size != args.points or s11.size != args.points or s21.size != args.points:
|
||||||
|
raise RuntimeError("Remote K209 sweep returned an unexpected point count")
|
||||||
|
if not np.all(np.isfinite(result.x)) or not np.all(np.isfinite(s11)) or not np.all(np.isfinite(s21)):
|
||||||
|
raise RuntimeError("Remote K209 sweep contains non-finite values")
|
||||||
|
if not np.all(np.diff(result.x) >= 0):
|
||||||
|
raise RuntimeError("Remote K209 frequency axis is not monotonic")
|
||||||
|
|
||||||
|
print(
|
||||||
|
"Remote K209 sweep OK: "
|
||||||
|
f"points={args.points}, first_hz={result.x[0]:.3f}, last_hz={result.x[-1]:.3f}, "
|
||||||
|
f"mean_abs_s11={float(np.mean(np.abs(s11))):.6g}, "
|
||||||
|
f"mean_abs_s21={float(np.mean(np.abs(s21))):.6g}"
|
||||||
|
)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from python_app.hardware_full.librevna_service import LibreVnaService
|
from python_app.hardware_full.single_radar_service import create_single_radar_service
|
||||||
from python_app.hardware_full.switch_service import SwitchService
|
from python_app.hardware_full.switch_service import SwitchService
|
||||||
from python_app.models.dataset_model import ComboKey, SweepCollection, TraceData
|
from python_app.models.dataset_model import ComboKey, SweepCollection, TraceData
|
||||||
from python_app.models.run_config_model import RunConfigModel
|
from python_app.models.run_config_model import RunConfigModel
|
||||||
@@ -26,7 +26,7 @@ def capture_calibration_set(
|
|||||||
|
|
||||||
combos = RunConfigModel.build_full_combos(config.input_switch.positions, config.output_switch.positions)
|
combos = RunConfigModel.build_full_combos(config.input_switch.positions, config.output_switch.positions)
|
||||||
|
|
||||||
radar = LibreVnaService(serial=config.radar.serial or None)
|
radar = create_single_radar_service(config)
|
||||||
input_switch = SwitchService(
|
input_switch = SwitchService(
|
||||||
name=config.input_switch.name,
|
name=config.input_switch.name,
|
||||||
positions=config.input_switch.positions,
|
positions=config.input_switch.positions,
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import time
|
|||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
from python_app.hardware_full.librevna_service import LibreVnaService
|
|
||||||
from python_app.hardware_full.multi_device_service import MultiDeviceLibreVnaService
|
from python_app.hardware_full.multi_device_service import MultiDeviceLibreVnaService
|
||||||
|
from python_app.hardware_full.single_radar_service import create_single_radar_service
|
||||||
from python_app.hardware_full.switch_service import SwitchService
|
from python_app.hardware_full.switch_service import SwitchService
|
||||||
from python_app.models.dataset_model import ComboKey, SweepCollection, TraceData
|
from python_app.models.dataset_model import ComboKey, SweepCollection, TraceData
|
||||||
from python_app.models.run_config_model import ComboModel, RunConfigModel
|
from python_app.models.run_config_model import ComboModel, RunConfigModel
|
||||||
@@ -100,7 +100,7 @@ class MultiRadarSequentialCaptureSession:
|
|||||||
self._input_switch = None
|
self._input_switch = None
|
||||||
self._output_switch = None
|
self._output_switch = None
|
||||||
else:
|
else:
|
||||||
self._radar = LibreVnaService(serial=base_config.radar.serial or None)
|
self._radar = create_single_radar_service(base_config)
|
||||||
self._input_switch = SwitchService(
|
self._input_switch = SwitchService(
|
||||||
name=base_config.input_switch.name,
|
name=base_config.input_switch.name,
|
||||||
positions=base_config.input_switch.positions,
|
positions=base_config.input_switch.positions,
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from python_app.hardware_full.librevna_service import LibreVnaService
|
|
||||||
from python_app.hardware_full.multi_device_service import MultiDeviceLibreVnaService
|
from python_app.hardware_full.multi_device_service import MultiDeviceLibreVnaService
|
||||||
|
from python_app.hardware_full.single_radar_service import create_single_radar_service
|
||||||
from python_app.hardware_full.switch_service import SwitchService
|
from python_app.hardware_full.switch_service import SwitchService
|
||||||
from python_app.models.dataset_model import ComboKey, SweepCollection, TraceData
|
from python_app.models.dataset_model import ComboKey, SweepCollection, TraceData
|
||||||
from python_app.models.run_config_model import RunConfigModel
|
from python_app.models.run_config_model import RunConfigModel
|
||||||
@@ -48,7 +48,7 @@ def capture_reference_set(
|
|||||||
|
|
||||||
combos = RunConfigModel.build_full_combos(config.input_switch.positions, config.output_switch.positions)
|
combos = RunConfigModel.build_full_combos(config.input_switch.positions, config.output_switch.positions)
|
||||||
|
|
||||||
radar = LibreVnaService(serial=config.radar.serial or None)
|
radar = create_single_radar_service(config)
|
||||||
input_switch = SwitchService(
|
input_switch = SwitchService(
|
||||||
name=config.input_switch.name,
|
name=config.input_switch.name,
|
||||||
positions=config.input_switch.positions,
|
positions=config.input_switch.positions,
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import time
|
|||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
from python_app.hardware_full.librevna_service import LibreVnaService
|
|
||||||
from python_app.hardware_full.multi_device_service import MultiDeviceLibreVnaService
|
from python_app.hardware_full.multi_device_service import MultiDeviceLibreVnaService
|
||||||
|
from python_app.hardware_full.single_radar_service import create_single_radar_service
|
||||||
from python_app.hardware_full.switch_service import SwitchService
|
from python_app.hardware_full.switch_service import SwitchService
|
||||||
from python_app.models.dataset_model import ComboKey, SweepCollection, TraceData
|
from python_app.models.dataset_model import ComboKey, SweepCollection, TraceData
|
||||||
from python_app.models.run_config_model import ComboModel, RunConfigModel
|
from python_app.models.run_config_model import ComboModel, RunConfigModel
|
||||||
@@ -70,7 +70,7 @@ class SequentialCaptureSession:
|
|||||||
self._input_switch = None
|
self._input_switch = None
|
||||||
self._output_switch = None
|
self._output_switch = None
|
||||||
else:
|
else:
|
||||||
self._radar = LibreVnaService(serial=config.radar.serial or None)
|
self._radar = create_single_radar_service(config)
|
||||||
self._input_switch = SwitchService(
|
self._input_switch = SwitchService(
|
||||||
name=config.input_switch.name,
|
name=config.input_switch.name,
|
||||||
positions=config.input_switch.positions,
|
positions=config.input_switch.positions,
|
||||||
|
|||||||
+48
-26
@@ -1,15 +1,14 @@
|
|||||||
{
|
{
|
||||||
"radar": {
|
"radar": {
|
||||||
"model": "librevna_multi",
|
"model": "compact_m_k209",
|
||||||
"serial": "207730885532",
|
"serial": "",
|
||||||
|
"remote_host": "127.0.0.1",
|
||||||
|
"remote_port": 50209,
|
||||||
"driver_mode": "native",
|
"driver_mode": "native",
|
||||||
"mock_signal_hz": 5000000.0,
|
"mock_signal_hz": 5000000.0,
|
||||||
"multi_device": {
|
"multi_device": {
|
||||||
"slave_serials": [
|
"slave_serials": [],
|
||||||
"20A1307D5532",
|
"force_external_reference": false,
|
||||||
"2072306C5532"
|
|
||||||
],
|
|
||||||
"force_external_reference": true,
|
|
||||||
"recovery_attempts": 3
|
"recovery_attempts": 3
|
||||||
},
|
},
|
||||||
"sweep": {
|
"sweep": {
|
||||||
@@ -23,10 +22,10 @@
|
|||||||
"switches": {
|
"switches": {
|
||||||
"port1": {
|
"port1": {
|
||||||
"name": "port1",
|
"name": "port1",
|
||||||
"driver_mode": "native",
|
"driver_mode": "mock",
|
||||||
"driver": "h7992",
|
"driver": "h7992",
|
||||||
"radar_port": 1,
|
"radar_port": 1,
|
||||||
"positions": 4,
|
"positions": 2,
|
||||||
"default_position": 0,
|
"default_position": 0,
|
||||||
"gpio_chip": "/dev/gpiochip0",
|
"gpio_chip": "/dev/gpiochip0",
|
||||||
"pin_a": 17,
|
"pin_a": 17,
|
||||||
@@ -35,7 +34,7 @@
|
|||||||
},
|
},
|
||||||
"port2": {
|
"port2": {
|
||||||
"name": "port2",
|
"name": "port2",
|
||||||
"driver_mode": "native",
|
"driver_mode": "mock",
|
||||||
"driver": "h7992",
|
"driver": "h7992",
|
||||||
"radar_port": 2,
|
"radar_port": 2,
|
||||||
"positions": 4,
|
"positions": 4,
|
||||||
@@ -49,7 +48,7 @@
|
|||||||
"run": {
|
"run": {
|
||||||
"settling_ms": 0,
|
"settling_ms": 0,
|
||||||
"idle_sleep_ms": 2,
|
"idle_sleep_ms": 2,
|
||||||
"continuous": true,
|
"continuous": false,
|
||||||
"processing_live_config_path": "python_app/runtime/processing_live.json",
|
"processing_live_config_path": "python_app/runtime/processing_live.json",
|
||||||
"locator_server": {
|
"locator_server": {
|
||||||
"device_id": 3,
|
"device_id": 3,
|
||||||
@@ -61,14 +60,38 @@
|
|||||||
"logger_name": "locator_runtime"
|
"logger_name": "locator_runtime"
|
||||||
},
|
},
|
||||||
"combos": [
|
"combos": [
|
||||||
{"input": 0, "output": 0},
|
{
|
||||||
{"input": 1, "output": 0},
|
"input": 0,
|
||||||
{"input": 2, "output": 0},
|
"output": 0
|
||||||
{"input": 3, "output": 0},
|
},
|
||||||
{"input": 0, "output": 1},
|
{
|
||||||
{"input": 1, "output": 1},
|
"input": 1,
|
||||||
{"input": 2, "output": 1},
|
"output": 0
|
||||||
{"input": 3, "output": 1}
|
},
|
||||||
|
{
|
||||||
|
"input": 2,
|
||||||
|
"output": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": 3,
|
||||||
|
"output": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": 0,
|
||||||
|
"output": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": 1,
|
||||||
|
"output": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": 2,
|
||||||
|
"output": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": 3,
|
||||||
|
"output": 1
|
||||||
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"preprocess": {
|
"preprocess": {
|
||||||
@@ -104,8 +127,7 @@
|
|||||||
},
|
},
|
||||||
"notch": {
|
"notch": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"bands_hz": [
|
"bands_hz": [],
|
||||||
],
|
|
||||||
"taper_width_hz": 40000000.0,
|
"taper_width_hz": 40000000.0,
|
||||||
"taper_type": "cosine"
|
"taper_type": "cosine"
|
||||||
}
|
}
|
||||||
@@ -144,27 +166,27 @@
|
|||||||
},
|
},
|
||||||
"rings": {
|
"rings": {
|
||||||
"raw": {
|
"raw": {
|
||||||
"name": "/radar_raw",
|
"name": "/radar_k209_local_raw",
|
||||||
"capacity": 50,
|
"capacity": 50,
|
||||||
"slot_size_bytes": 2097152
|
"slot_size_bytes": 2097152
|
||||||
},
|
},
|
||||||
"raw_tap": {
|
"raw_tap": {
|
||||||
"name": "/radar_raw_tap",
|
"name": "/radar_k209_local_raw_tap",
|
||||||
"capacity": 50,
|
"capacity": 50,
|
||||||
"slot_size_bytes": 2097152
|
"slot_size_bytes": 2097152
|
||||||
},
|
},
|
||||||
"preprocessed": {
|
"preprocessed": {
|
||||||
"name": "/radar_preprocessed",
|
"name": "/radar_k209_local_preprocessed",
|
||||||
"capacity": 50,
|
"capacity": 50,
|
||||||
"slot_size_bytes": 2097152
|
"slot_size_bytes": 2097152
|
||||||
},
|
},
|
||||||
"preprocessed_tap": {
|
"preprocessed_tap": {
|
||||||
"name": "/radar_preprocessed_tap",
|
"name": "/radar_k209_local_preprocessed_tap",
|
||||||
"capacity": 50,
|
"capacity": 50,
|
||||||
"slot_size_bytes": 2097152
|
"slot_size_bytes": 2097152
|
||||||
},
|
},
|
||||||
"results": {
|
"results": {
|
||||||
"name": "/radar_results",
|
"name": "/radar_k209_local_results",
|
||||||
"capacity": 50,
|
"capacity": 50,
|
||||||
"slot_size_bytes": 2097152
|
"slot_size_bytes": 2097152
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,194 @@
|
|||||||
|
{
|
||||||
|
"radar": {
|
||||||
|
"model": "compact_m_k209",
|
||||||
|
"serial": "",
|
||||||
|
"remote_host": "192.168.1.10",
|
||||||
|
"remote_port": 50209,
|
||||||
|
"driver_mode": "native",
|
||||||
|
"mock_signal_hz": 5000000.0,
|
||||||
|
"multi_device": {
|
||||||
|
"slave_serials": [],
|
||||||
|
"force_external_reference": false,
|
||||||
|
"recovery_attempts": 3
|
||||||
|
},
|
||||||
|
"sweep": {
|
||||||
|
"start_hz": 1000000.0,
|
||||||
|
"stop_hz": 6000000000.0,
|
||||||
|
"points": 201,
|
||||||
|
"if_bandwidth_hz": 50000.0,
|
||||||
|
"stimulus_power_dbm": -10.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"switches": {
|
||||||
|
"port1": {
|
||||||
|
"name": "port1",
|
||||||
|
"driver_mode": "native",
|
||||||
|
"driver": "h7992",
|
||||||
|
"radar_port": 1,
|
||||||
|
"positions": 2,
|
||||||
|
"default_position": 0,
|
||||||
|
"gpio_chip": "/dev/gpiochip0",
|
||||||
|
"pin_a": 17,
|
||||||
|
"pin_b": 27,
|
||||||
|
"invert_logic": false
|
||||||
|
},
|
||||||
|
"port2": {
|
||||||
|
"name": "port2",
|
||||||
|
"driver_mode": "native",
|
||||||
|
"driver": "h7992",
|
||||||
|
"radar_port": 2,
|
||||||
|
"positions": 4,
|
||||||
|
"default_position": 0,
|
||||||
|
"gpio_chip": "/dev/gpiochip0",
|
||||||
|
"pin_a": 22,
|
||||||
|
"pin_b": 23,
|
||||||
|
"invert_logic": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"run": {
|
||||||
|
"settling_ms": 0,
|
||||||
|
"idle_sleep_ms": 2,
|
||||||
|
"continuous": true,
|
||||||
|
"processing_live_config_path": "python_app/runtime/processing_live.json",
|
||||||
|
"locator_server": {
|
||||||
|
"device_id": 3,
|
||||||
|
"protocol_version": 1,
|
||||||
|
"host": "0.0.0.0",
|
||||||
|
"port": 8888,
|
||||||
|
"max_payload_bytes": 65536,
|
||||||
|
"client_queue_size": 32,
|
||||||
|
"logger_name": "locator_runtime"
|
||||||
|
},
|
||||||
|
"combos": [
|
||||||
|
{
|
||||||
|
"input": 0,
|
||||||
|
"output": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": 1,
|
||||||
|
"output": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": 2,
|
||||||
|
"output": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": 3,
|
||||||
|
"output": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": 0,
|
||||||
|
"output": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": 1,
|
||||||
|
"output": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": 2,
|
||||||
|
"output": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": 3,
|
||||||
|
"output": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"preprocess": {
|
||||||
|
"s21": {
|
||||||
|
"calibration": {
|
||||||
|
"set_name": "",
|
||||||
|
"bundle_path": ""
|
||||||
|
},
|
||||||
|
"reference": {
|
||||||
|
"set_name": "",
|
||||||
|
"bundle_path": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"s11": {
|
||||||
|
"calibration": {
|
||||||
|
"open": {
|
||||||
|
"set_name": "",
|
||||||
|
"bundle_path": ""
|
||||||
|
},
|
||||||
|
"short": {
|
||||||
|
"set_name": "",
|
||||||
|
"bundle_path": ""
|
||||||
|
},
|
||||||
|
"load": {
|
||||||
|
"set_name": "",
|
||||||
|
"bundle_path": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"reference": {
|
||||||
|
"set_name": "",
|
||||||
|
"bundle_path": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notch": {
|
||||||
|
"enabled": true,
|
||||||
|
"bands_hz": [],
|
||||||
|
"taper_width_hz": 40000000.0,
|
||||||
|
"taper_type": "cosine"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"gpr": {
|
||||||
|
"mode": "point",
|
||||||
|
"relative_permittivity": 1.0,
|
||||||
|
"tx_geometry": [
|
||||||
|
{
|
||||||
|
"output_pos": 0,
|
||||||
|
"x_m": 0.905
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"output_pos": 1,
|
||||||
|
"x_m": -0.905
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"rx_geometry": [
|
||||||
|
{
|
||||||
|
"input_pos": 0,
|
||||||
|
"x_m": -0.18
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input_pos": 1,
|
||||||
|
"x_m": 0.485
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input_pos": 2,
|
||||||
|
"x_m": -0.49
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input_pos": 3,
|
||||||
|
"x_m": 0.185
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"rings": {
|
||||||
|
"raw": {
|
||||||
|
"name": "/radar_raw",
|
||||||
|
"capacity": 50,
|
||||||
|
"slot_size_bytes": 2097152
|
||||||
|
},
|
||||||
|
"raw_tap": {
|
||||||
|
"name": "/radar_raw_tap",
|
||||||
|
"capacity": 50,
|
||||||
|
"slot_size_bytes": 2097152
|
||||||
|
},
|
||||||
|
"preprocessed": {
|
||||||
|
"name": "/radar_preprocessed",
|
||||||
|
"capacity": 50,
|
||||||
|
"slot_size_bytes": 2097152
|
||||||
|
},
|
||||||
|
"preprocessed_tap": {
|
||||||
|
"name": "/radar_preprocessed_tap",
|
||||||
|
"capacity": 50,
|
||||||
|
"slot_size_bytes": 2097152
|
||||||
|
},
|
||||||
|
"results": {
|
||||||
|
"name": "/radar_results",
|
||||||
|
"capacity": 50,
|
||||||
|
"slot_size_bytes": 2097152
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,194 @@
|
|||||||
|
{
|
||||||
|
"radar": {
|
||||||
|
"model": "compact_m_k209",
|
||||||
|
"serial": "",
|
||||||
|
"remote_host": "127.0.0.1",
|
||||||
|
"remote_port": 50209,
|
||||||
|
"driver_mode": "native",
|
||||||
|
"mock_signal_hz": 5000000.0,
|
||||||
|
"multi_device": {
|
||||||
|
"slave_serials": [],
|
||||||
|
"force_external_reference": false,
|
||||||
|
"recovery_attempts": 3
|
||||||
|
},
|
||||||
|
"sweep": {
|
||||||
|
"start_hz": 1000000.0,
|
||||||
|
"stop_hz": 6000000000.0,
|
||||||
|
"points": 201,
|
||||||
|
"if_bandwidth_hz": 50000.0,
|
||||||
|
"stimulus_power_dbm": -10.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"switches": {
|
||||||
|
"port1": {
|
||||||
|
"name": "port1",
|
||||||
|
"driver_mode": "mock",
|
||||||
|
"driver": "h7992",
|
||||||
|
"radar_port": 1,
|
||||||
|
"positions": 2,
|
||||||
|
"default_position": 0,
|
||||||
|
"gpio_chip": "/dev/gpiochip0",
|
||||||
|
"pin_a": 17,
|
||||||
|
"pin_b": 27,
|
||||||
|
"invert_logic": false
|
||||||
|
},
|
||||||
|
"port2": {
|
||||||
|
"name": "port2",
|
||||||
|
"driver_mode": "mock",
|
||||||
|
"driver": "h7992",
|
||||||
|
"radar_port": 2,
|
||||||
|
"positions": 4,
|
||||||
|
"default_position": 0,
|
||||||
|
"gpio_chip": "/dev/gpiochip0",
|
||||||
|
"pin_a": 22,
|
||||||
|
"pin_b": 23,
|
||||||
|
"invert_logic": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"run": {
|
||||||
|
"settling_ms": 0,
|
||||||
|
"idle_sleep_ms": 2,
|
||||||
|
"continuous": false,
|
||||||
|
"processing_live_config_path": "python_app/runtime/processing_live.json",
|
||||||
|
"locator_server": {
|
||||||
|
"device_id": 3,
|
||||||
|
"protocol_version": 1,
|
||||||
|
"host": "0.0.0.0",
|
||||||
|
"port": 8888,
|
||||||
|
"max_payload_bytes": 65536,
|
||||||
|
"client_queue_size": 32,
|
||||||
|
"logger_name": "locator_runtime"
|
||||||
|
},
|
||||||
|
"combos": [
|
||||||
|
{
|
||||||
|
"input": 0,
|
||||||
|
"output": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": 1,
|
||||||
|
"output": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": 2,
|
||||||
|
"output": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": 3,
|
||||||
|
"output": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": 0,
|
||||||
|
"output": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": 1,
|
||||||
|
"output": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": 2,
|
||||||
|
"output": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": 3,
|
||||||
|
"output": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"preprocess": {
|
||||||
|
"s21": {
|
||||||
|
"calibration": {
|
||||||
|
"set_name": "",
|
||||||
|
"bundle_path": ""
|
||||||
|
},
|
||||||
|
"reference": {
|
||||||
|
"set_name": "",
|
||||||
|
"bundle_path": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"s11": {
|
||||||
|
"calibration": {
|
||||||
|
"open": {
|
||||||
|
"set_name": "",
|
||||||
|
"bundle_path": ""
|
||||||
|
},
|
||||||
|
"short": {
|
||||||
|
"set_name": "",
|
||||||
|
"bundle_path": ""
|
||||||
|
},
|
||||||
|
"load": {
|
||||||
|
"set_name": "",
|
||||||
|
"bundle_path": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"reference": {
|
||||||
|
"set_name": "",
|
||||||
|
"bundle_path": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notch": {
|
||||||
|
"enabled": true,
|
||||||
|
"bands_hz": [],
|
||||||
|
"taper_width_hz": 40000000.0,
|
||||||
|
"taper_type": "cosine"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"gpr": {
|
||||||
|
"mode": "point",
|
||||||
|
"relative_permittivity": 1.0,
|
||||||
|
"tx_geometry": [
|
||||||
|
{
|
||||||
|
"output_pos": 0,
|
||||||
|
"x_m": 0.905
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"output_pos": 1,
|
||||||
|
"x_m": -0.905
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"rx_geometry": [
|
||||||
|
{
|
||||||
|
"input_pos": 0,
|
||||||
|
"x_m": -0.18
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input_pos": 1,
|
||||||
|
"x_m": 0.485
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input_pos": 2,
|
||||||
|
"x_m": -0.49
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input_pos": 3,
|
||||||
|
"x_m": 0.185
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"rings": {
|
||||||
|
"raw": {
|
||||||
|
"name": "/radar_k209_local_raw",
|
||||||
|
"capacity": 50,
|
||||||
|
"slot_size_bytes": 2097152
|
||||||
|
},
|
||||||
|
"raw_tap": {
|
||||||
|
"name": "/radar_k209_local_raw_tap",
|
||||||
|
"capacity": 50,
|
||||||
|
"slot_size_bytes": 2097152
|
||||||
|
},
|
||||||
|
"preprocessed": {
|
||||||
|
"name": "/radar_k209_local_preprocessed",
|
||||||
|
"capacity": 50,
|
||||||
|
"slot_size_bytes": 2097152
|
||||||
|
},
|
||||||
|
"preprocessed_tap": {
|
||||||
|
"name": "/radar_k209_local_preprocessed_tap",
|
||||||
|
"capacity": 50,
|
||||||
|
"slot_size_bytes": 2097152
|
||||||
|
},
|
||||||
|
"results": {
|
||||||
|
"name": "/radar_k209_local_results",
|
||||||
|
"capacity": 50,
|
||||||
|
"slot_size_bytes": 2097152
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,192 @@
|
|||||||
|
{
|
||||||
|
"radar": {
|
||||||
|
"model": "librevna",
|
||||||
|
"serial": "",
|
||||||
|
"driver_mode": "native",
|
||||||
|
"mock_signal_hz": 5000000.0,
|
||||||
|
"multi_device": {
|
||||||
|
"slave_serials": [],
|
||||||
|
"force_external_reference": false,
|
||||||
|
"recovery_attempts": 3
|
||||||
|
},
|
||||||
|
"sweep": {
|
||||||
|
"start_hz": 1000000.0,
|
||||||
|
"stop_hz": 6000000000.0,
|
||||||
|
"points": 201,
|
||||||
|
"if_bandwidth_hz": 50000.0,
|
||||||
|
"stimulus_power_dbm": -10.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"switches": {
|
||||||
|
"port1": {
|
||||||
|
"name": "port1",
|
||||||
|
"driver_mode": "native",
|
||||||
|
"driver": "h7992",
|
||||||
|
"radar_port": 1,
|
||||||
|
"positions": 2,
|
||||||
|
"default_position": 0,
|
||||||
|
"gpio_chip": "/dev/gpiochip0",
|
||||||
|
"pin_a": 17,
|
||||||
|
"pin_b": 27,
|
||||||
|
"invert_logic": false
|
||||||
|
},
|
||||||
|
"port2": {
|
||||||
|
"name": "port2",
|
||||||
|
"driver_mode": "native",
|
||||||
|
"driver": "h7992",
|
||||||
|
"radar_port": 2,
|
||||||
|
"positions": 4,
|
||||||
|
"default_position": 0,
|
||||||
|
"gpio_chip": "/dev/gpiochip0",
|
||||||
|
"pin_a": 22,
|
||||||
|
"pin_b": 23,
|
||||||
|
"invert_logic": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"run": {
|
||||||
|
"settling_ms": 0,
|
||||||
|
"idle_sleep_ms": 2,
|
||||||
|
"continuous": true,
|
||||||
|
"processing_live_config_path": "python_app/runtime/processing_live.json",
|
||||||
|
"locator_server": {
|
||||||
|
"device_id": 3,
|
||||||
|
"protocol_version": 1,
|
||||||
|
"host": "0.0.0.0",
|
||||||
|
"port": 8888,
|
||||||
|
"max_payload_bytes": 65536,
|
||||||
|
"client_queue_size": 32,
|
||||||
|
"logger_name": "locator_runtime"
|
||||||
|
},
|
||||||
|
"combos": [
|
||||||
|
{
|
||||||
|
"input": 0,
|
||||||
|
"output": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": 1,
|
||||||
|
"output": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": 2,
|
||||||
|
"output": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": 3,
|
||||||
|
"output": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": 0,
|
||||||
|
"output": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": 1,
|
||||||
|
"output": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": 2,
|
||||||
|
"output": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": 3,
|
||||||
|
"output": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"preprocess": {
|
||||||
|
"s21": {
|
||||||
|
"calibration": {
|
||||||
|
"set_name": "",
|
||||||
|
"bundle_path": ""
|
||||||
|
},
|
||||||
|
"reference": {
|
||||||
|
"set_name": "",
|
||||||
|
"bundle_path": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"s11": {
|
||||||
|
"calibration": {
|
||||||
|
"open": {
|
||||||
|
"set_name": "",
|
||||||
|
"bundle_path": ""
|
||||||
|
},
|
||||||
|
"short": {
|
||||||
|
"set_name": "",
|
||||||
|
"bundle_path": ""
|
||||||
|
},
|
||||||
|
"load": {
|
||||||
|
"set_name": "",
|
||||||
|
"bundle_path": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"reference": {
|
||||||
|
"set_name": "",
|
||||||
|
"bundle_path": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notch": {
|
||||||
|
"enabled": true,
|
||||||
|
"bands_hz": [],
|
||||||
|
"taper_width_hz": 40000000.0,
|
||||||
|
"taper_type": "cosine"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"gpr": {
|
||||||
|
"mode": "point",
|
||||||
|
"relative_permittivity": 1.0,
|
||||||
|
"tx_geometry": [
|
||||||
|
{
|
||||||
|
"output_pos": 0,
|
||||||
|
"x_m": 0.905
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"output_pos": 1,
|
||||||
|
"x_m": -0.905
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"rx_geometry": [
|
||||||
|
{
|
||||||
|
"input_pos": 0,
|
||||||
|
"x_m": -0.18
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input_pos": 1,
|
||||||
|
"x_m": 0.485
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input_pos": 2,
|
||||||
|
"x_m": -0.49
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input_pos": 3,
|
||||||
|
"x_m": 0.185
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"rings": {
|
||||||
|
"raw": {
|
||||||
|
"name": "/radar_raw",
|
||||||
|
"capacity": 50,
|
||||||
|
"slot_size_bytes": 2097152
|
||||||
|
},
|
||||||
|
"raw_tap": {
|
||||||
|
"name": "/radar_raw_tap",
|
||||||
|
"capacity": 50,
|
||||||
|
"slot_size_bytes": 2097152
|
||||||
|
},
|
||||||
|
"preprocessed": {
|
||||||
|
"name": "/radar_preprocessed",
|
||||||
|
"capacity": 50,
|
||||||
|
"slot_size_bytes": 2097152
|
||||||
|
},
|
||||||
|
"preprocessed_tap": {
|
||||||
|
"name": "/radar_preprocessed_tap",
|
||||||
|
"capacity": 50,
|
||||||
|
"slot_size_bytes": 2097152
|
||||||
|
},
|
||||||
|
"results": {
|
||||||
|
"name": "/radar_results",
|
||||||
|
"capacity": 50,
|
||||||
|
"slot_size_bytes": 2097152
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,195 @@
|
|||||||
|
{
|
||||||
|
"radar": {
|
||||||
|
"model": "librevna_multi",
|
||||||
|
"serial": "207730885532",
|
||||||
|
"driver_mode": "native",
|
||||||
|
"mock_signal_hz": 5000000.0,
|
||||||
|
"multi_device": {
|
||||||
|
"slave_serials": [
|
||||||
|
"20A1307D5532",
|
||||||
|
"2072306C5532"
|
||||||
|
],
|
||||||
|
"force_external_reference": true,
|
||||||
|
"recovery_attempts": 3
|
||||||
|
},
|
||||||
|
"sweep": {
|
||||||
|
"start_hz": 1000000.0,
|
||||||
|
"stop_hz": 6000000000.0,
|
||||||
|
"points": 201,
|
||||||
|
"if_bandwidth_hz": 50000.0,
|
||||||
|
"stimulus_power_dbm": -10.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"switches": {
|
||||||
|
"port1": {
|
||||||
|
"name": "port1",
|
||||||
|
"driver_mode": "mock",
|
||||||
|
"driver": "h7992",
|
||||||
|
"radar_port": 1,
|
||||||
|
"positions": 2,
|
||||||
|
"default_position": 0,
|
||||||
|
"gpio_chip": "/dev/gpiochip0",
|
||||||
|
"pin_a": 17,
|
||||||
|
"pin_b": 27,
|
||||||
|
"invert_logic": false
|
||||||
|
},
|
||||||
|
"port2": {
|
||||||
|
"name": "port2",
|
||||||
|
"driver_mode": "mock",
|
||||||
|
"driver": "h7992",
|
||||||
|
"radar_port": 2,
|
||||||
|
"positions": 4,
|
||||||
|
"default_position": 0,
|
||||||
|
"gpio_chip": "/dev/gpiochip0",
|
||||||
|
"pin_a": 22,
|
||||||
|
"pin_b": 23,
|
||||||
|
"invert_logic": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"run": {
|
||||||
|
"settling_ms": 0,
|
||||||
|
"idle_sleep_ms": 2,
|
||||||
|
"continuous": true,
|
||||||
|
"processing_live_config_path": "python_app/runtime/processing_live.json",
|
||||||
|
"locator_server": {
|
||||||
|
"device_id": 3,
|
||||||
|
"protocol_version": 1,
|
||||||
|
"host": "0.0.0.0",
|
||||||
|
"port": 8888,
|
||||||
|
"max_payload_bytes": 65536,
|
||||||
|
"client_queue_size": 32,
|
||||||
|
"logger_name": "locator_runtime"
|
||||||
|
},
|
||||||
|
"combos": [
|
||||||
|
{
|
||||||
|
"input": 0,
|
||||||
|
"output": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": 1,
|
||||||
|
"output": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": 2,
|
||||||
|
"output": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": 3,
|
||||||
|
"output": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": 0,
|
||||||
|
"output": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": 1,
|
||||||
|
"output": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": 2,
|
||||||
|
"output": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": 3,
|
||||||
|
"output": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"preprocess": {
|
||||||
|
"s21": {
|
||||||
|
"calibration": {
|
||||||
|
"set_name": "",
|
||||||
|
"bundle_path": ""
|
||||||
|
},
|
||||||
|
"reference": {
|
||||||
|
"set_name": "",
|
||||||
|
"bundle_path": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"s11": {
|
||||||
|
"calibration": {
|
||||||
|
"open": {
|
||||||
|
"set_name": "",
|
||||||
|
"bundle_path": ""
|
||||||
|
},
|
||||||
|
"short": {
|
||||||
|
"set_name": "",
|
||||||
|
"bundle_path": ""
|
||||||
|
},
|
||||||
|
"load": {
|
||||||
|
"set_name": "",
|
||||||
|
"bundle_path": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"reference": {
|
||||||
|
"set_name": "",
|
||||||
|
"bundle_path": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notch": {
|
||||||
|
"enabled": true,
|
||||||
|
"bands_hz": [],
|
||||||
|
"taper_width_hz": 40000000.0,
|
||||||
|
"taper_type": "cosine"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"gpr": {
|
||||||
|
"mode": "point",
|
||||||
|
"relative_permittivity": 1.0,
|
||||||
|
"tx_geometry": [
|
||||||
|
{
|
||||||
|
"output_pos": 0,
|
||||||
|
"x_m": 0.905
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"output_pos": 1,
|
||||||
|
"x_m": -0.905
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"rx_geometry": [
|
||||||
|
{
|
||||||
|
"input_pos": 0,
|
||||||
|
"x_m": -0.18
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input_pos": 1,
|
||||||
|
"x_m": 0.485
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input_pos": 2,
|
||||||
|
"x_m": -0.49
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input_pos": 3,
|
||||||
|
"x_m": 0.185
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"rings": {
|
||||||
|
"raw": {
|
||||||
|
"name": "/radar_raw",
|
||||||
|
"capacity": 50,
|
||||||
|
"slot_size_bytes": 2097152
|
||||||
|
},
|
||||||
|
"raw_tap": {
|
||||||
|
"name": "/radar_raw_tap",
|
||||||
|
"capacity": 50,
|
||||||
|
"slot_size_bytes": 2097152
|
||||||
|
},
|
||||||
|
"preprocessed": {
|
||||||
|
"name": "/radar_preprocessed",
|
||||||
|
"capacity": 50,
|
||||||
|
"slot_size_bytes": 2097152
|
||||||
|
},
|
||||||
|
"preprocessed_tap": {
|
||||||
|
"name": "/radar_preprocessed_tap",
|
||||||
|
"capacity": 50,
|
||||||
|
"slot_size_bytes": 2097152
|
||||||
|
},
|
||||||
|
"results": {
|
||||||
|
"name": "/radar_results",
|
||||||
|
"capacity": 50,
|
||||||
|
"slot_size_bytes": 2097152
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user