Push Tracker
RIA_Example/Applications/Example_Application.json

869 lines
43 KiB
JSON

{
"app_name": "new_blocks.json",
"ops": [
{
"id": "69bd3deeff031ee6e72c0a9d-1776345526320",
"name": "pluto_source",
"type": "source",
"description": "PlutoSDR RX source operator. Acquires raw IQ samples from an Analog Devices PlutoSDR (AD9361) via libiio and emits them as shared complex vectors.",
"position": {
"x": 68.5360685699228,
"y": 357.7951264299131
},
"class_name": "PlutoSourceOp",
"is_builtin": false,
"inputs": [],
"outputs": [
{
"name": "iq_samples",
"type": "std::shared_ptr<std::vector<ComplexType>>"
}
],
"specs": [
{
"parameter": "ip_addr_",
"key": "ip_addr",
"headline": "Pluto IP Address",
"description": "IP address of the PlutoSDR device",
"type": "std::string",
"default": "192.168.3.1"
},
{
"parameter": "bandwidth_mhz_",
"key": "bandwidth_mhz",
"headline": "RF bandwidth",
"description": "RF analog bandwidth in MHz",
"type": "double",
"default": 2
},
{
"parameter": "sample_rate_mhz_",
"key": "sample_rate_mhz",
"headline": "RX sample rate",
"description": "RX baseband sample rate in MS/s",
"type": "double",
"default": 2.5
},
{
"parameter": "lo_frequency_ghz_",
"key": "lo_frequency_ghz",
"headline": "RX LO Frequency",
"description": "RF local oscillator frequency in GHz",
"type": "double",
"default": 0.0999
},
{
"parameter": "rx_gain_db_",
"key": "rx_gain_db",
"headline": "RX Gain",
"description": "Receiver gain in dB",
"type": "double",
"default": 20
},
{
"parameter": "rf_port_",
"key": "rf_port",
"headline": "RF port",
"description": "RX channel index",
"type": "int",
"default": 0
},
{
"parameter": "buffer_size_",
"key": "buffer_size",
"headline": "Buffer size",
"description": "Number of IQ sample pairs per buffer",
"type": "int",
"default": 4096
},
{
"parameter": "poll_rate_ms_",
"key": "poll_rate_ms",
"headline": "Poll Rate",
"description": "Minimum milliseconds between buffer refills",
"type": "int",
"default": 10
}
],
"dependencies": [
"#include <iio.h>",
"#include <iostream>",
"#include <cmath>",
"#include <cstdlib>"
],
"private_members": [
"struct iio_context* context_ = nullptr;",
"struct iio_device* rx_device_ = nullptr;",
"struct iio_channel* ch_i_ = nullptr;",
"struct iio_channel* ch_q_ = nullptr;",
"struct iio_buffer* buffer_ = nullptr;",
"std::chrono::high_resolution_clock::time_point last_read_time_;",
"std::atomic<double> runtime_rx_freq_ghz_{3.425};",
"std::atomic<double> runtime_rx_gain_db_{30.0};",
"std::atomic<bool> rx_params_dirty_{false};"
],
"has_initialize": true,
"initialize_body": "HOLOSCAN_LOG_INFO(\"Initializing Pluto RX stream\");\n Operator::initialize();\n runtime_rx_freq_ghz_.store(lo_frequency_ghz_.get());\n runtime_rx_gain_db_.store(rx_gain_db_.get());\n struct stream_cfg rxcfg {};\n rxcfg.bw_hz = MHZ(bandwidth_mhz_.get());\n rxcfg.fs_hz = MHZ(sample_rate_mhz_.get());\n rxcfg.lo_hz = GHZ(lo_frequency_ghz_.get());\n rxcfg.rfport = \"A_BALANCED\";\n const int port_index = rf_port_.get();\n std::string uri = \"ip:\" + ip_addr_.get();\n context_ = iio_create_context_from_uri(uri.c_str());\n if (!context_) return;\n if (!get_ad9361_stream_dev(context_, RX, &rx_device_)) return;\n if (!cfg_ad9361_streaming_ch(context_, &rxcfg, RX, port_index)) return;\n get_ad9361_stream_ch(RX, rx_device_, port_index, &ch_i_);\n get_ad9361_stream_ch(RX, rx_device_, port_index + 1, &ch_q_);\n iio_channel_enable(ch_i_);\n iio_channel_enable(ch_q_);\n buffer_ = iio_device_create_buffer(rx_device_, buffer_size_.get(), false);",
"compute_body": "auto samples_ptr = std::make_shared<std::vector<ComplexType>>();\n samples_ptr->reserve(buffer_size_.get());\n if (context_ && buffer_) {\n iio_buffer_refill(buffer_);\n const size_t step = iio_buffer_step(buffer_);\n const char* start = static_cast<char*>(iio_buffer_first(buffer_, ch_i_));\n const char* end = static_cast<char*>(iio_buffer_end(buffer_));\n for (const char* p = start; p < end; p += step) {\n const auto raw = reinterpret_cast<const int16_t*>(p);\n samples_ptr->emplace_back(raw[0], raw[1]);\n }\n } else {\n for (int i = 0; i < buffer_size_.get(); ++i) {\n samples_ptr->emplace_back(static_cast<float>(rand()%100), static_cast<float>(rand()%100));\n }\n }\n op_output.emit(samples_ptr, \"iq_samples\");",
"class": "",
"compute": ""
},
{
"id": "69bd3deeff031ee6e72c0aa0-1776345543621",
"name": "preprocessor",
"type": "preprocessing",
"description": "Converts complex IQ samples into a GXF tensor suitable for ML inference. Separates real and imaginary components into interleaved float format with shape [1, 2, N].",
"position": {
"x": 414.64197818208436,
"y": 544.7366288284782
},
"class_name": "PreprocessorOp",
"is_builtin": false,
"inputs": [
{
"name": "iq_samples",
"type": "std::shared_ptr<std::vector<ComplexType>>"
}
],
"outputs": [
{
"name": "in_tensor",
"type": "gxf::Entity"
}
],
"specs": [
{
"parameter": "allocator_",
"key": "allocator",
"headline": "Alloc",
"description": "Memory allocator for tensor creation",
"type": "std::shared_ptr<holoscan::Allocator>",
"default": "pool_resource"
},
{
"parameter": "in_tensor_name_",
"key": "in_tensor_name",
"headline": "Name",
"description": "Name of the output tensor in the GXF entity",
"type": "std::string",
"default": "input"
}
],
"dependencies": [
"#include <gxf/std/tensor.hpp>"
],
"private_members": [],
"has_initialize": false,
"initialize_body": null,
"compute_body": "auto maybe_s = op_input.receive<std::shared_ptr<std::vector<ComplexType>>>(\"iq_samples\"); if(!maybe_s) return; const auto& s = *maybe_s.value(); auto [re,im] = convert_and_normalize_separate(s); auto ent = nvidia::gxf::Entity::New(context.context()).value(); auto tensor = ent.add<nvidia::gxf::Tensor>(in_tensor_name_.get().c_str()).value(); auto alloc = nvidia::gxf::Handle<nvidia::gxf::Allocator>::Create(context.context(), allocator_->gxf_cid()).value(); tensor->template reshape<float>(nvidia::gxf::Shape({1,2,static_cast<int>(s.size())}), nvidia::gxf::MemoryStorageType::kHost, alloc); float* d = tensor->template data<float>().value(); for(size_t i=0;i<s.size();++i){ d[2*i]=re[i]; d[2*i+1]=im[i]; } op_output.emit(ent);",
"class": "",
"compute": ""
},
{
"id": "69bd3deeff031ee6e72c0aa1-1776345548184",
"name": "spectrogram",
"type": "preprocessing",
"description": "FFT-based spectrogram generator using FFTW3. Produces RGBA waterfall rows with configurable colormap, windowing, and ADC input scaling. Emits only new rows for efficient incremental rendering.",
"position": {
"x": 503.6426850135131,
"y": 41.98771759801264
},
"class_name": "SpectrogramOp",
"is_builtin": false,
"inputs": [
{
"name": "iq_samples",
"type": "std::shared_ptr<std::vector<ComplexType>>"
}
],
"outputs": [
{
"name": "new_rows",
"type": "std::shared_ptr<std::vector<uint8_t>>"
},
{
"name": "spectrogram_width",
"type": "int"
},
{
"name": "spectrogram_height",
"type": "int"
},
{
"name": "offset_row",
"type": "int"
},
{
"name": "num_new_rows",
"type": "int"
}
],
"specs": [
{
"parameter": "fft_size_",
"key": "fft_size",
"headline": "FFT Size",
"description": "Number of FFT points",
"type": "int",
"default": 2048
},
{
"parameter": "history_depth_",
"key": "history_depth",
"headline": "History Depth",
"description": "Number of rows in the circular waterfall buffer",
"type": "int",
"default": 256
},
{
"parameter": "hop_size_",
"key": "hop_size",
"headline": "Hop Size",
"description": "Samples between successive FFT frames",
"type": "int",
"default": 2048
},
{
"parameter": "db_min_",
"key": "db_min",
"headline": "Min dB",
"description": "Floor of the dB dynamic range for colormap mapping",
"type": "float",
"default": -80
},
{
"parameter": "db_max_",
"key": "db_max",
"headline": "Max dB",
"description": "Ceiling of the dB dynamic range for colormap mapping",
"type": "float",
"default": 0
},
{
"parameter": "input_scale_",
"key": "input_scale",
"headline": "Input Scale",
"description": "Scale factor for raw input samples, baked into window coefficients (e.g. 1/2048 for PlutoSDR int16, 1.0 for normalized float)",
"type": "float",
"default": 0.000488281
}
],
"dependencies": [
"#include <complex>",
"#include <vector>",
"#include <cmath>",
"#include <algorithm>",
"#include <cstring>",
"#define M_PI 3.14159265358979323846",
"#include <fftw3.h>"
],
"private_members": [
"fftwf_plan fft_plan_ = nullptr;",
"fftwf_complex* fft_in_ = nullptr;",
"fftwf_complex* fft_out_ = nullptr;",
"std::vector<float> window_coeffs_;",
"std::vector<uint8_t> texture_data_;",
"std::vector<uint32_t> colormap_lut_;",
"int write_row_ = 0;",
"static float fast_log2f(float x) { union { float f; uint32_t i; } u; u.f = x; return (float)((int32_t)u.i - 1064866805) * 8.2629582881927490e-8f; }",
"int prev_write_row_ = 0;"
],
"has_initialize": true,
"initialize_body": "holoscan::Operator::initialize(); int n = fft_size_.get(); int depth = history_depth_.get(); fft_in_ = fftwf_alloc_complex(n); fft_out_ = fftwf_alloc_complex(n); fft_plan_ = fftwf_plan_dft_1d(n, fft_in_, fft_out_, FFTW_FORWARD, FFTW_MEASURE); window_coeffs_.resize(n); const float adc_scale = input_scale_.get(); for (int i = 0; i < n; ++i) { window_coeffs_[i] = 0.5f * (1.0f - std::cos(2.0f * M_PI * i / (n - 1))) * adc_scale; } write_row_ = 0; /* Purple-to-yellow (Plasma) colormap */ colormap_lut_.resize(1024); const float stops[][4] = { {0.00f, 13.0f, 8.0f, 135.0f}, {0.25f, 126.0f, 3.0f, 168.0f}, {0.50f, 203.0f, 70.0f, 121.0f}, {0.75f, 249.0f, 149.0f, 64.0f}, {1.00f, 240.0f, 249.0f, 33.0f} }; const int ns = 5; for (int i = 0; i < 1024; ++i) { float t = i / 1023.0f; int seg = 0; for (int s = 0; s < ns - 1; ++s) { if (t >= stops[s][0]) seg = s; } float lt = (t - stops[seg][0]) / (stops[seg+1][0] - stops[seg][0]); lt = std::clamp(lt, 0.0f, 1.0f); uint8_t r = static_cast<uint8_t>(stops[seg][1] + (stops[seg+1][1] - stops[seg][1]) * lt); uint8_t g = static_cast<uint8_t>(stops[seg][2] + (stops[seg+1][2] - stops[seg][2]) * lt); uint8_t b = static_cast<uint8_t>(stops[seg][3] + (stops[seg+1][3] - stops[seg][3]) * lt); colormap_lut_[i] = (255u << 24) | (b << 16) | (g << 8) | r; } texture_data_.assign(n * depth * 4, 0); HOLOSCAN_LOG_INFO(\"FFTW plan created for N={}\", n);",
"compute_body": "auto s_in = op_input.receive<std::shared_ptr<std::vector<ComplexType>>>(\"iq_samples\"); if (!s_in || s_in.value()->empty()) return; const auto& samples = *s_in.value(); const int n = fft_size_.get(); const int hop = hop_size_.get(); const int depth = history_depth_.get(); const float db_min = db_min_.get(); const float db_range = db_max_.get() - db_min; const float inv_n2 = 1.0f / (float)(n * n); const float log2_to_db = 3.01029995663981f; prev_write_row_ = write_row_; for (size_t start = 0; start + n <= samples.size(); start += hop) { for (int i = 0; i < n; ++i) { fft_in_[i][0] = samples[start + i].real() * window_coeffs_[i]; fft_in_[i][1] = samples[start + i].imag() * window_coeffs_[i]; } fftwf_execute(fft_plan_); uint32_t* row_ptr = reinterpret_cast<uint32_t*>(texture_data_.data()) + (write_row_ * n); for (int i = 0; i < n; ++i) { int si = (i + n/2) % n; float re = fft_out_[si][0], im = fft_out_[si][1]; float mag_sq = (re * re + im * im) * inv_n2; float db = log2_to_db * fast_log2f(mag_sq + 1e-20f); float norm = std::clamp((db - db_min) / db_range, 0.0f, 1.0f); row_ptr[i] = colormap_lut_[static_cast<int>(norm * 1023.0f)]; } write_row_ = (write_row_ + 1) % depth; } /* Emit only the new rows */ int num_new = (write_row_ - prev_write_row_ + depth) % depth; if (num_new > 0) { auto new_rows = std::make_shared<std::vector<uint8_t>>(num_new * n * 4); for (int r = 0; r < num_new; ++r) { int src_row = (prev_write_row_ + r) % depth; std::memcpy(new_rows->data() + r * n * 4, texture_data_.data() + src_row * n * 4, n * 4); } op_output.emit(new_rows, \"new_rows\"); op_output.emit(n, \"spectrogram_width\"); op_output.emit(depth, \"spectrogram_height\"); op_output.emit(write_row_, \"offset_row\"); op_output.emit(num_new, \"num_new_rows\"); }",
"class": "",
"compute": ""
},
{
"id": "69bd3deeff031ee6e72c0a9b-1776345577288",
"name": "inference_builtin",
"type": "inference",
"description": "Built-in Holoscan InferenceOp. Runs ONNX Runtime or TensorRT inference on GXF tensor entities.",
"position": {
"x": 651.8201808383262,
"y": 542.6597535841762
},
"class_name": "InferenceOp",
"is_builtin": true,
"inputs": [
{
"name": "receivers",
"type": "gxf::Entity"
}
],
"outputs": [
{
"name": "transmitter",
"type": "gxf::Entity"
}
],
"specs": [
{
"parameter": "backend",
"key": "backend",
"headline": "Backend",
"description": "Inference backend: onnxrt or trt",
"type": "std::string",
"default": "onnxrt"
},
{
"parameter": "model_path_map_",
"key": "model_path_map",
"headline": "Model Path Map",
"description": "Map of model name to ONNX/TRT model file path",
"type": "std::map<std::string, std::string>",
"default": "interference_recognition_fp32.onnx"
},
{
"parameter": "pre_processor_map_",
"key": "pre_processor_map",
"headline": "Pre-processor Map",
"description": "Map of model name to input tensor names",
"type": "std::map<std::string, std::vector<std::string>>",
"default": "input"
},
{
"parameter": "inference_map_",
"key": "inference_map",
"headline": "Inference Map",
"description": "Map of model name to output tensor names",
"type": "std::map<std::string, std::vector<std::string>>",
"default": "output"
},
{
"parameter": "input_on_cuda",
"key": "input_on_cuda",
"headline": "Input on CUDA",
"description": null,
"type": "bool",
"default": false
},
{
"parameter": "output_on_cuda",
"key": "output_on_cuda",
"headline": "Output on CUDA",
"description": null,
"type": "bool",
"default": false
},
{
"parameter": "transmit_on_cuda",
"key": "transmit_on_cuda",
"headline": "Transmit on CUDA",
"description": null,
"type": "bool",
"default": false
},
{
"parameter": "allocator",
"key": "allocator",
"headline": "Allocator",
"description": null,
"type": "std::shared_ptr<holoscan::Allocator>",
"default": "pool_resource"
}
],
"dependencies": [],
"private_members": [],
"has_initialize": false,
"initialize_body": null,
"compute_body": "",
"class": "",
"compute": ""
},
{
"id": "69bd3deeff031ee6e72c0a9f-1776345584342",
"name": "postprocessor",
"type": "postprocessing",
"description": "Extracts classification logits from inference output tensor, applies softmax, and emits probabilities as a shared float vector. Supports both CPU and CUDA tensors.",
"position": {
"x": 1008.3165490811291,
"y": 545.574956436883
},
"class_name": "PostprocessorOp",
"is_builtin": false,
"inputs": [
{
"name": "out_tensor",
"type": "gxf::Entity"
}
],
"outputs": [
{
"name": "predictions",
"type": "std::shared_ptr<std::vector<float>>"
}
],
"specs": [
{
"parameter": "out_tensor_name_",
"key": "out_tensor_name",
"headline": "Output Tensor Name",
"description": "Name of the output tensor to extract from the GXF entity",
"type": "std::string",
"default": "output"
}
],
"dependencies": [
"#include <gxf/std/tensor.hpp>",
"#include <cstring>",
"#include <vector>",
"#include <memory>",
"#include <cuda_runtime_api.h>"
],
"private_members": [],
"has_initialize": false,
"initialize_body": null,
"compute_body": "auto maybe_in_message = op_input.receive<gxf::Entity>(\"out_tensor\");\n if (!maybe_in_message) return;\n auto maybe_tensor = maybe_in_message.value().get<holoscan::Tensor>(out_tensor_name_.get().c_str());\n if (!maybe_tensor) { HOLOSCAN_LOG_ERROR(\"Output tensor '{}' not found\", out_tensor_name_.get()); return; }\n const auto& shape = maybe_tensor->shape();\n if (shape.empty()) { HOLOSCAN_LOG_ERROR(\"Invalid tensor shape: empty\"); return; }\n const auto classes_dim = shape.back();\n if (classes_dim <= 0 || classes_dim > 65536) { HOLOSCAN_LOG_ERROR(\"Invalid class dimension: {}\", classes_dim); return; }\n const size_t num_classes = static_cast<size_t>(classes_dim);\n const size_t needed_bytes = num_classes * sizeof(float);\n if (maybe_tensor->nbytes() < needed_bytes) {\n HOLOSCAN_LOG_ERROR(\"Output tensor '{}' too small: {} bytes (need {})\", out_tensor_name_.get(), maybe_tensor->nbytes(), needed_bytes);\n return;\n }\n DLDevice dev = maybe_tensor->device();\n DLDataType dtype = maybe_tensor->dtype();\n if (dtype.code != kDLFloat || dtype.bits != 32) {\n HOLOSCAN_LOG_ERROR(\"Output tensor '{}' is not float32 (code={}, bits={})\", out_tensor_name_.get(), static_cast<int>(dtype.code), static_cast<int>(dtype.bits));\n return;\n }\n const void* raw_ptr = maybe_tensor->data();\n if (!raw_ptr) { HOLOSCAN_LOG_ERROR(\"Null tensor data pointer\"); return; }\n std::vector<float> host_logits(num_classes);\n if (dev.device_type == kDLCPU) {\n std::memcpy(host_logits.data(), raw_ptr, needed_bytes);\n } else if (dev.device_type == kDLCUDA) {\n cudaError_t err = cudaMemcpy(host_logits.data(), raw_ptr, needed_bytes, cudaMemcpyDeviceToHost);\n if (err != cudaSuccess) {\n HOLOSCAN_LOG_ERROR(\"cudaMemcpy DeviceToHost failed for '{}': {}\", out_tensor_name_.get(), cudaGetErrorString(err));\n return;\n }\n } else {\n HOLOSCAN_LOG_ERROR(\"Unsupported output tensor device type {} for '{}'\", static_cast<int>(dev.device_type), out_tensor_name_.get());\n return;\n }\n auto probs = std::make_shared<std::vector<float>>(softmax(host_logits.data(), num_classes));\n if (probs->empty()) { HOLOSCAN_LOG_ERROR(\"Softmax output is empty\"); return; }\n op_output.emit(probs, \"predictions\");",
"class": "",
"compute": ""
},
{
"id": "69bd3deeff031ee6e72c0aa2-1776345597091",
"name": "spectrogram_dashboard",
"type": "sink",
"description": "Flexible HTTP dashboard server. Accepts optional spectrogram RGBA rows, prediction maps, and emits TX control messages. Serves spectrogram data and status JSON via a POSIX HTTP server. All inputs are optional — connect only what you need.",
"position": {
"x": 1534.7404760043617,
"y": 27.35610520025739
},
"class_name": "SpectrogramDashboardOp",
"is_builtin": false,
"inputs": [
{
"name": "new_rows",
"type": "std::shared_ptr<std::vector<uint8_t>>"
},
{
"name": "spectrogram_width",
"type": "int"
},
{
"name": "spectrogram_height",
"type": "int"
},
{
"name": "offset_row",
"type": "int"
},
{
"name": "num_new_rows",
"type": "int"
},
{
"name": "predictions",
"type": "std::map<std::string, float>"
}
],
"outputs": [
{
"name": "tx_control_message",
"type": "std::shared_ptr<RadioControlMessage>"
}
],
"specs": [
{
"parameter": "websocket_port_",
"key": "websocket_port",
"headline": "HTTP Port",
"description": "Port number for the HTTP data server",
"type": "int",
"default": 8080
},
{
"parameter": "web_root_path_",
"key": "web_root_path",
"headline": "Web Root Path",
"description": "Root directory for web assets (used for file-based data export)",
"type": "std::string",
"default": "workspace/app/web"
},
{
"parameter": "update_rate_hz_",
"key": "update_rate_hz",
"headline": "Update Rate (Hz)",
"description": "Target update rate for the dashboard data",
"type": "int",
"default": 30
}
],
"dependencies": [
"#include <sys/socket.h>",
"#include <netinet/in.h>",
"#include <arpa/inet.h>",
"#include <unistd.h>",
"#include <iomanip>",
"#include <sstream>",
"#include <algorithm>",
"#include <cmath>",
"#include <thread>",
"#include <atomic>",
"#include <mutex>",
"#include <set>",
"#include <chrono>",
"#include <fstream>",
"#include \"../utils/utils.hpp\""
],
"private_members": [
"int server_socket_ = -1;",
"std::thread server_thread_;",
"std::atomic<bool> server_running_{false};",
"std::map<std::string, float> smoothed_predictions_;",
"std::vector<uint8_t> current_rgba_buffer_;",
"int current_width_ = 0;",
"int current_height_ = 0;",
"int current_offset_ = 0;",
"std::mutex data_mutex_;",
"double tx_center_freq_mhz_ = 3425.0;",
"double tx_gain_db_ = -10.0;",
"bool tx_enabled_ = false;",
"int tx_signal_type_ = 6;",
"std::vector<std::pair<int,double>> pending_msgs_;",
"std::chrono::steady_clock::time_point last_update_;",
"const std::chrono::milliseconds update_interval_{33};",
"void cleanup() { if (server_running_.load()) { server_running_.store(false); if (server_socket_ >= 0) { close(server_socket_); } if (server_thread_.joinable()) { server_thread_.join(); } } }",
"std::string generate_json_response() { std::stringstream ss; ss << \"{\\\"width\\\":\" << current_width_ << \",\"; ss << \"\\\"height\\\":\" << current_height_ << \",\"; ss << \"\\\"offset_row\\\":\" << current_offset_ << \",\"; ss << \"\\\"tx_enabled\\\":\" << (tx_enabled_ ? \"true\" : \"false\") << \",\"; ss << \"\\\"tx_freq_mhz\\\":\" << tx_center_freq_mhz_ << \",\"; ss << \"\\\"tx_gain_db\\\":\" << tx_gain_db_ << \",\"; ss << \"\\\"tx_signal_type\\\":\" << tx_signal_type_ << \",\"; ss << \"\\\"predictions\\\":{\"; bool first = true; for (const auto& [label, prob] : smoothed_predictions_) { if (!first) ss << \",\"; ss << \"\\\"\" << label << \"\\\":\" << prob; first = false; } ss << \"}}\"; return ss.str(); }",
"void handle_client(int client_socket) { char req_buf[4096] = {0}; int bytes_read = ::read(client_socket, req_buf, sizeof(req_buf) - 1); if (bytes_read <= 0) { close(client_socket); return; } std::string request(req_buf, bytes_read); if (request.find(\"OPTIONS \") == 0) { std::string resp = \"HTTP/1.1 204 No Content\\r\\nAccess-Control-Allow-Origin: *\\r\\nAccess-Control-Allow-Methods: GET, POST, OPTIONS\\r\\nAccess-Control-Allow-Headers: Content-Type\\r\\n\\r\\n\"; send(client_socket, resp.c_str(), resp.length(), MSG_NOSIGNAL); return; } if (request.find(\"POST /tx_control\") != std::string::npos) { std::string body; auto bp = request.find(\"\\r\\n\\r\\n\"); if (bp != std::string::npos) body = request.substr(bp + 4); { std::lock_guard<std::mutex> lock(data_mutex_); if (body.find(\"\\\"toggle_tx\\\"\") != std::string::npos) { tx_enabled_ = !tx_enabled_; pending_msgs_.push_back({tx_enabled_ ? 4 : 5, 0.0}); } if (body.find(\"\\\"set_signal_type\\\"\") != std::string::npos) { auto vp = body.find(\"\\\"value\\\":\"); if (vp != std::string::npos) { tx_signal_type_ = std::atoi(body.c_str() + vp + 8); pending_msgs_.push_back({7, static_cast<double>(tx_signal_type_)}); } } if (body.find(\"\\\"set_freq\\\"\") != std::string::npos) { auto vp = body.find(\"\\\"value\\\":\"); if (vp != std::string::npos) { double fv = std::atof(body.c_str() + vp + 8); tx_center_freq_mhz_ = fv; pending_msgs_.push_back({1, fv}); } } if (body.find(\"\\\"set_gain\\\"\") != std::string::npos) { auto vp = body.find(\"\\\"value\\\":\"); if (vp != std::string::npos) { double gv = std::atof(body.c_str() + vp + 8); tx_gain_db_ = gv; pending_msgs_.push_back({3, gv}); } } } std::string rb; { std::lock_guard<std::mutex> lock(data_mutex_); rb = generate_json_response(); } std::string resp = \"HTTP/1.1 200 OK\\r\\nContent-Type: application/json\\r\\nAccess-Control-Allow-Origin: *\\r\\nContent-Length: \" + std::to_string(rb.size()) + \"\\r\\n\\r\\n\" + rb; send(client_socket, resp.c_str(), resp.length(), MSG_NOSIGNAL); return; } bool is_raw = (request.find(\"GET /spectrogram.raw\") != std::string::npos); std::lock_guard<std::mutex> lock(data_mutex_); if (is_raw && !current_rgba_buffer_.empty()) { std::string header = \"HTTP/1.1 200 OK\\r\\nContent-Type: application/octet-stream\\r\\nAccess-Control-Allow-Origin: *\\r\\nCache-Control: no-store\\r\\nContent-Length: \" + std::to_string(current_rgba_buffer_.size()) + \"\\r\\n\\r\\n\"; send(client_socket, header.c_str(), header.length(), MSG_NOSIGNAL); size_t total_sent = 0; while (total_sent < current_rgba_buffer_.size()) { ssize_t sent = send(client_socket, current_rgba_buffer_.data() + total_sent, current_rgba_buffer_.size() - total_sent, MSG_NOSIGNAL); if (sent <= 0) break; total_sent += sent; } } else { std::string body = generate_json_response(); std::string response = \"HTTP/1.1 200 OK\\r\\nContent-Type: application/json\\r\\nAccess-Control-Allow-Origin: *\\r\\nCache-Control: no-store\\r\\nContent-Length: \" + std::to_string(body.size()) + \"\\r\\n\\r\\n\" + body; send(client_socket, response.c_str(), response.length(), MSG_NOSIGNAL); } }",
"void run_server() { while (server_running_.load()) { struct sockaddr_in address; int addrlen = sizeof(address); int client_socket = accept(server_socket_, (struct sockaddr*)&address, (socklen_t*)&addrlen); if (client_socket >= 0) { handle_client(client_socket); close(client_socket); } } }",
"void write_data_file() { std::lock_guard<std::mutex> lock(data_mutex_); if (!current_rgba_buffer_.empty() && !web_root_path_.get().empty()) { std::string path = web_root_path_.get() + \"/public/spectrogram.raw\"; std::ofstream file(path, std::ios::binary); file.write(reinterpret_cast<const char*>(current_rgba_buffer_.data()), current_rgba_buffer_.size()); } }"
],
"has_initialize": true,
"initialize_body": "holoscan::Operator::initialize();\n \n server_socket_ = socket(AF_INET, SOCK_STREAM, 0);\n if (server_socket_ < 0) {\n HOLOSCAN_LOG_ERROR(\"Failed to create socket\");\n return;\n }\n \n int opt = 1;\n setsockopt(server_socket_, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));\n \n struct sockaddr_in address;\n address.sin_family = AF_INET;\n address.sin_addr.s_addr = INADDR_ANY;\n address.sin_port = htons(websocket_port_.get());\n \n if (bind(server_socket_, (struct sockaddr*)&address, sizeof(address)) < 0) {\n HOLOSCAN_LOG_ERROR(\"Failed to bind socket to port {}\", websocket_port_.get());\n close(server_socket_);\n return;\n }\n \n if (listen(server_socket_, 3) < 0) {\n HOLOSCAN_LOG_ERROR(\"Failed to listen on socket\");\n close(server_socket_);\n return;\n }\n \n server_running_.store(true);\n server_thread_ = std::thread([this]() { run_server(); });\n \n last_update_ = std::chrono::steady_clock::now();\n HOLOSCAN_LOG_INFO(\"HTTP server started on port {}\", websocket_port_.get());",
"compute_body": "auto spec_in = op_input.receive<std::shared_ptr<std::vector<uint8_t>>>(\"new_rows\");\n auto w_in = op_input.receive<int>(\"spectrogram_width\");\n auto h_in = op_input.receive<int>(\"spectrogram_height\");\n auto offset_in = op_input.receive<int>(\"offset_row\");\n auto nr_in = op_input.receive<int>(\"num_new_rows\");\n auto p_in = op_input.receive<std::map<std::string, float>>(\"predictions\");\n \n {\n std::lock_guard<std::mutex> lock(data_mutex_);\n if (spec_in && w_in && h_in && offset_in && nr_in) {\n int w = w_in.value();\n int h = h_in.value();\n int num_new = nr_in.value();\n size_t buf_size = static_cast<size_t>(w) * h * 4;\n if (current_rgba_buffer_.size() != buf_size) current_rgba_buffer_.resize(buf_size, 0);\n current_width_ = w;\n current_height_ = h;\n current_offset_ = offset_in.value();\n if (num_new > 0) {\n const auto& new_data = *spec_in.value();\n int row_bytes = w * 4;\n int start_row = (current_offset_ - num_new + h) % h;\n for (int r = 0; r < num_new; ++r) {\n int dst_row = (start_row + r) % h;\n std::memcpy(current_rgba_buffer_.data() + dst_row * row_bytes,\n new_data.data() + r * row_bytes, row_bytes);\n }\n }\n }\n if (p_in) {\n const float alpha = 0.15f; auto raw = p_in.value(); if (smoothed_predictions_.empty()) { smoothed_predictions_ = raw; } else { for (auto& [label, val] : raw) { smoothed_predictions_[label] = alpha * val + (1.0f - alpha) * smoothed_predictions_[label]; } }\n }\n }\n \n { std::lock_guard<std::mutex> lock2(data_mutex_);\n if (!pending_msgs_.empty()) {\n auto [t, v] = pending_msgs_.front();\n pending_msgs_.erase(pending_msgs_.begin());\n auto msg = std::make_shared<RadioControlMessage>();\n msg->type = static_cast<RadioControlMessage::Type>(t);\n msg->value = v;\n op_output.emit(msg, \"tx_control_message\");\n }\n }",
"class": "",
"compute": ""
},
{
"id": "69bd3deeff031ee6e72c0a9c-1776345675756",
"name": "model_mapper",
"type": "postprocessing",
"description": "Maps numeric prediction probabilities to human-readable class labels. Accepts a shared float vector and emits a label-to-probability map.",
"position": {
"x": 1319.1869982595924,
"y": 551.0619693437972
},
"class_name": "ModelMapperOp",
"is_builtin": false,
"inputs": [
{
"name": "predictions",
"type": "std::shared_ptr<std::vector<float>>"
}
],
"outputs": [
{
"name": "labeled_predictions",
"type": "std::map<std::string, float>"
}
],
"specs": [
{
"parameter": "labels_",
"key": "labels",
"headline": "Class Labels",
"description": "Ordered list of class label strings corresponding to prediction indices",
"type": "std::vector<std::string>",
"default": "[\"Bleedover\", \"Collisions\", \"Parallel\", \"Birdies\", \"CTNB\", \"LFM\", \"None\", \"Jamming\"]"
}
],
"dependencies": [
"#include <map>",
"#include <string>",
"#include <vector>",
"#include <memory>"
],
"private_members": [],
"has_initialize": false,
"initialize_body": null,
"compute_body": "auto p = op_input.receive<std::shared_ptr<std::vector<float>>>(\"predictions\"); if(!p) return; const auto& probs = *p.value(); size_t n = probs.size(); std::map<std::string,float> r; for(size_t i=0;i<n;++i){ r[(i<labels_.get().size())?labels_.get()[i]:std::to_string(i)] = probs[i]; } op_output.emit(r, \"labeled_predictions\");",
"class": "",
"compute": ""
},
{
"id": "69d80d08aabdb365e3b84e14-1776361788176",
"name": "PlutoTXOp",
"type": "sink",
"description": "Transmits IQ samples through an ADALM-Pluto SDR via libiio.",
"position": {
"x": 2040.5281414494543,
"y": 50.98952706451257
},
"class_name": "PlutoTXOp",
"is_builtin": false,
"inputs": [
{
"name": "signal_in",
"type": "std::shared_ptr<std::vector<std::complex<float>>>"
}
],
"outputs": [],
"specs": [
{
"parameter": "uri_",
"key": "uri",
"headline": "Pluto URI",
"description": null,
"type": "std::string",
"default": "192.168.3.1"
},
{
"parameter": "sample_rate_",
"key": "sample_rate",
"headline": "TX sample rate in Hz",
"description": null,
"type": "int64_t",
"default": 2500000
},
{
"parameter": "center_freq_",
"key": "center_freq",
"headline": "TX center frequency in Hz",
"description": null,
"type": "int64_t",
"default": "2415500000"
},
{
"parameter": "bandwidth_",
"key": "bandwidth",
"headline": "TX RF bandwidth in Hz",
"description": null,
"type": "int64_t",
"default": 2000000
},
{
"parameter": "attenuation_",
"key": "attenuation",
"headline": "TX attenuation in mdB (negative)",
"description": null,
"type": "int64_t",
"default": "0"
}
],
"dependencies": [
"#include <ria/operator.hpp>",
"#include <iio.h>",
"#include <complex>",
"#include <vector>"
],
"private_members": [
"struct iio_context* ctx_ = nullptr;",
"struct iio_device* phy_ = nullptr;",
"struct iio_device* txdev_ = nullptr;",
"struct iio_channel* tx0_i_ = nullptr;",
"struct iio_channel* tx0_q_ = nullptr;",
"struct iio_buffer* txbuf_ = nullptr;"
],
"has_initialize": true,
"initialize_body": "ria::Operator::initialize();\nctx_ = iio_create_context_from_uri(uri_.get().c_str());\nif (!ctx_) { std::cerr << \"Cannot connect to Pluto at \" << uri_.get() << std::endl; return; }\nphy_ = iio_context_find_device(ctx_, \"ad9361-phy\");\niio_channel_attr_write_longlong(iio_device_find_channel(phy_, \"altvoltage1\", true), \"frequency\", center_freq_.get());\niio_channel_attr_write_longlong(iio_device_find_channel(phy_, \"voltage0\", true), \"sampling_frequency\", sample_rate_.get());\niio_channel_attr_write_longlong(iio_device_find_channel(phy_, \"voltage0\", true), \"rf_bandwidth\", bandwidth_.get());\niio_channel_attr_write_longlong(iio_device_find_channel(phy_, \"voltage0\", true), \"hardwaregain\", attenuation_.get());\ntxdev_ = iio_context_find_device(ctx_, \"cf-ad9361-dds-core-lpc\");\ntx0_i_ = iio_device_find_channel(txdev_, \"voltage0\", true);\ntx0_q_ = iio_device_find_channel(txdev_, \"voltage1\", true);\niio_channel_enable(tx0_i_);\niio_channel_enable(tx0_q_);\ntxbuf_ = iio_device_create_buffer(txdev_, 1024, false);",
"compute_body": "auto signal = input.receive<std::shared_ptr<std::vector<std::complex<float>>>>(\"signal_in\").value();\nint16_t* buf = static_cast<int16_t*>(iio_buffer_first(txbuf_, tx0_i_));\nsize_t n = std::min(signal->size(), static_cast<size_t>(1024));\nfor (size_t i = 0; i < n; ++i) {\n buf[2*i] = static_cast<int16_t>((*signal)[i].real() * 2048.0f);\n buf[2*i+1] = static_cast<int16_t>((*signal)[i].imag() * 2048.0f);\n}\niio_buffer_push(txbuf_);",
"class": "",
"compute": ""
}
],
"flows": [
{
"upstream": "69bd3deeff031ee6e72c0a9d-1776345526320",
"downstream": "69bd3deeff031ee6e72c0aa0-1776345543621",
"port_pairs": {
"iq_samples": "iq_samples"
},
"sourcePosition": "right",
"targetPosition": "left",
"label": "iq_samples → iq_samples",
"animated": false,
"style": {
"stroke": "#4183c4",
"strokeWidth": 1.5,
"strokeDasharray": "5 4",
"opacity": 0.7
}
},
{
"upstream": "69bd3deeff031ee6e72c0aa0-1776345543621",
"downstream": "69bd3deeff031ee6e72c0a9b-1776345577288",
"port_pairs": {
"in_tensor": "receivers"
},
"sourcePosition": "right",
"targetPosition": "left",
"label": "in_tensor → receivers",
"animated": false,
"style": {
"stroke": "#4183c4",
"strokeWidth": 1.5,
"strokeDasharray": "5 4",
"opacity": 0.7
}
},
{
"upstream": "69bd3deeff031ee6e72c0a9b-1776345577288",
"downstream": "69bd3deeff031ee6e72c0a9f-1776345584342",
"port_pairs": {
"transmitter": "out_tensor"
},
"sourcePosition": "right",
"targetPosition": "left",
"label": "transmitter → out_tensor",
"animated": false,
"style": {
"stroke": "#4183c4",
"strokeWidth": 1.5,
"strokeDasharray": "5 4",
"opacity": 0.7
}
},
{
"upstream": "69bd3deeff031ee6e72c0aa1-1776345548184",
"downstream": "69bd3deeff031ee6e72c0aa2-1776345597091",
"port_pairs": {
"num_new_rows": "num_new_rows"
},
"sourcePosition": "right",
"targetPosition": "left",
"label": "num_new_rows → num_new_rows",
"animated": false,
"style": {
"stroke": "#4183c4",
"strokeWidth": 1.5,
"strokeDasharray": "5 4",
"opacity": 0.7
}
},
{
"upstream": "69bd3deeff031ee6e72c0aa1-1776345548184",
"downstream": "69bd3deeff031ee6e72c0aa2-1776345597091",
"port_pairs": {
"offset_row": "offset_row"
},
"sourcePosition": "right",
"targetPosition": "left",
"label": "offset_row → offset_row",
"animated": false,
"style": {
"stroke": "#4183c4",
"strokeWidth": 1.5,
"strokeDasharray": "5 4",
"opacity": 0.7
}
},
{
"upstream": "69bd3deeff031ee6e72c0aa1-1776345548184",
"downstream": "69bd3deeff031ee6e72c0aa2-1776345597091",
"port_pairs": {
"spectrogram_height": "spectrogram_height"
},
"sourcePosition": "right",
"targetPosition": "left",
"label": "spectrogram_height → spectrogram_height",
"animated": false,
"style": {
"stroke": "#4183c4",
"strokeWidth": 1.5,
"strokeDasharray": "5 4",
"opacity": 0.7
}
},
{
"upstream": "69bd3deeff031ee6e72c0aa1-1776345548184",
"downstream": "69bd3deeff031ee6e72c0aa2-1776345597091",
"port_pairs": {
"spectrogram_width": "spectrogram_width"
},
"sourcePosition": "right",
"targetPosition": "left",
"label": "spectrogram_width → spectrogram_width",
"animated": false,
"style": {
"stroke": "#4183c4",
"strokeWidth": 1.5,
"strokeDasharray": "5 4",
"opacity": 0.7
}
},
{
"upstream": "69bd3deeff031ee6e72c0aa1-1776345548184",
"downstream": "69bd3deeff031ee6e72c0aa2-1776345597091",
"port_pairs": {
"new_rows": "new_rows"
},
"sourcePosition": "right",
"targetPosition": "left",
"label": "new_rows → new_rows",
"animated": false,
"style": {
"stroke": "#4183c4",
"strokeWidth": 1.5,
"strokeDasharray": "5 4",
"opacity": 0.7
}
},
{
"upstream": "69bd3deeff031ee6e72c0a9f-1776345584342",
"downstream": "69bd3deeff031ee6e72c0a9c-1776345675756",
"port_pairs": {
"predictions": "predictions"
},
"sourcePosition": "right",
"targetPosition": "left",
"label": "predictions → predictions",
"animated": false,
"style": {
"stroke": "#4183c4",
"strokeWidth": 1.5,
"strokeDasharray": "5 4",
"opacity": 0.7
}
},
{
"upstream": "69bd3deeff031ee6e72c0a9c-1776345675756",
"downstream": "69bd3deeff031ee6e72c0aa2-1776345597091",
"port_pairs": {
"labeled_predictions": "predictions"
},
"sourcePosition": "right",
"targetPosition": "left",
"label": "labeled_predictions → predictions",
"animated": false,
"style": {
"stroke": "#4183c4",
"strokeWidth": 1.5,
"strokeDasharray": "5 4",
"opacity": 0.7
}
},
{
"upstream": "69bd3deeff031ee6e72c0aa2-1776345597091",
"downstream": "69d80d08aabdb365e3b84e14-1776361788176",
"port_pairs": {
"tx_control_message": "signal_in"
},
"sourcePosition": "right",
"targetPosition": "left",
"label": "tx_control_message → signal_in",
"animated": false,
"style": {
"stroke": "#4183c4",
"strokeWidth": 1.5,
"strokeDasharray": "5 4",
"opacity": 0.7
}
},
{
"upstream": "69bd3deeff031ee6e72c0a9d-1776345526320",
"downstream": "69bd3deeff031ee6e72c0aa1-1776345548184",
"port_pairs": {
"iq_samples": "iq_samples"
},
"sourcePosition": "right",
"targetPosition": "left",
"label": "iq_samples → iq_samples",
"animated": false,
"style": {
"stroke": "#4183c4",
"strokeWidth": 1.5,
"strokeDasharray": "5 4",
"opacity": 0.7
}
}
]
}