added remote k209 setup

This commit is contained in:
Ayzen
2026-04-29 16:32:39 +03:00
parent e05c06bcbe
commit 5a70235ef3
30 changed files with 2373 additions and 95 deletions
+1
View File
@@ -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 \
+30
View File
@@ -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);
@@ -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
@@ -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
View File
@@ -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
+213
View File
@@ -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.
+324
View File
@@ -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}")
+4
View File
@@ -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": {
+2
View File
@@ -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})"
+151
View File
@@ -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())
+2 -2
View File
@@ -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,
+2 -2
View File
@@ -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
View File
@@ -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
} }
+194
View File
@@ -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
}
}
}
+192
View File
@@ -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
}
}
}
+195
View File
@@ -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
}
}
}