{
  "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
      }
    }
  ]
}