"description":"PlutoSDR RX source operator. Acquires raw IQ samples from an Analog Devices PlutoSDR (AD9361) via libiio and emits them as shared complex vectors.",
"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].",
"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.",
"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",
"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",