Loading...

Advanced Patterns

This page covers SDK features beyond the basics: receiving data from clients, performance profiling, and utilities for timing, serialization, and device configuration.

Consumer Taps

Standard taps send data from your app to a client. Consumer taps do the reverse: they receive data from a client into your app. This enables bidirectional communication, such as receiving control commands or parameter updates while your app is running. The callback runs in its own thread, so if you share state with your main() loop, use appropriate synchronization (e.g., std::mutex, std::atomic). Consumer taps are automatically started on creation and are stopped when the app shuts down.

The example below creates a consumer tap that receives synapse::Tensor messages on a named channel.

bool MyApp::setup() {
  // Create a consumer tap that receives Tensor messages
  if (!create_consumer_tap<synapse::Tensor>("control_input",
      [this](const synapse::Tensor& message) {
        // This callback runs in a separate thread
        spdlog::info("Received control input with {} dimensions",
                     message.shape().size());
        // Process the incoming message
      })) {
    spdlog::error("Failed to create consumer tap");
    return false;
  }

  return true;
}

Function Profiling

The SDK includes a built-in profiler for measuring the execution time of your functions. This is useful for identifying bottlenecks without adding external tooling.

Setup

Register each profile label with add_profile(), then enable profiling with a reporting interval:

bool MyApp::setup() {
  // Register every label you plan to instrument before enabling profiling.
  // start_profile() / stop_profile() on an unregistered label log an error and record nothing.
  add_profile("frame_acquisition");
  add_profile("signal_processing");
  add_profile("publish");

  // Report profiling stats every 5 seconds
  enable_function_profiling(std::chrono::milliseconds(5000));
  return true;
}

Instrumenting Code

Wrap sections of code with start_profile() and stop_profile() using the labels you registered above:

void MyApp::main() {
  while (node_running_) {
    start_profile("frame_acquisition");
    auto messages = data_reader_->receive_multipart();
    stop_profile("frame_acquisition");

    if (messages.empty()) {
      std::this_thread::sleep_for(std::chrono::microseconds(1));
      continue;
    }

    start_profile("signal_processing");
    for (auto& message : messages) {
      // Filter and detect spikes
    }
    stop_profile("signal_processing");

    start_profile("publish");
    publish_tap("output", output_tensor);
    stop_profile("publish");
  }
}

The profiler tracks call count, min/max/average execution time, and latest sample for each named profile. Results are published to the app_performance_summary tap at the configured interval. To print a specific profile to your app logs on demand:

print_profile("signal_processing");

Configuration Utilities

The SDK provides helper functions for reading the device's signal chain configuration. These are useful when your app needs to adapt its behavior based on which nodes are present or how they are configured.

#include <synapse-app-sdk/utils/config/config.hpp>

Find nodes by type

// Get all broadband source node IDs
auto node_ids = synapse::get_node_ids_for_type(
    device_configuration_, synapse::NodeType::kBroadbandSource);

// Get full node configs for all application nodes
auto app_nodes = synapse::get_nodes_for_type(
    device_configuration_, synapse::NodeType::kApplication);

Find a specific node

// Look up a node by its ID
auto node = synapse::get_node_by_id(device_configuration_, 1);
if (node.has_value()) {
  // Use node.value() to access the config
}

Time Utilities

The SDK provides clock functions and a Timer class for rate limiting and interval-based operations:

#include <synapse-app-sdk/utils/time/time.hpp>

Timer

Timer tracks elapsed time against a configurable interval:

// Create a timer that fires every 0.1 seconds (10 Hz)
synapse::Timer publish_timer(0.1);

void MyApp::main() {
  while (node_running_) {
    // Process data...

    // Only publish at 10 Hz
    if (publish_timer.reset_if_elapsed()) {
      publish_tap("output", result);
    }
  }
}

Timer methods:

  • has_elapsed() — returns true if the interval has passed
  • elapsed_seconds() — returns time since last reset
  • reset() — resets the timer
  • reset_if_elapsed() — resets and returns true if the interval had passed, false otherwise

Clock Functions

// Get current steady clock time (monotonic, for measuring durations)
auto now_ns = synapse::get_steady_clock_now();
auto now_us = synapse::get_steady_clock_now<std::chrono::microseconds>();

// Get current system clock time (wall clock, for timestamps)
auto sys_ns = synapse::get_system_clock_now();

Data Serialization

Creating outgoing messages

Convert protobuf to ZMQ messages, or pack raw data into Tensor format:

// Protobuf to ZMQ
auto zmq_msg = synapse::protobuf_to_zmq_message(my_protobuf);

// Pack a vector into Tensor binary format
std::vector<float> data = {1.0f, 2.0f, 3.0f};
std::string packed = synapse::pack_tensor_data(data);

// Pack inline values
std::string packed = synapse::pack_tensor_data<float>({x, y});

Log Streaming

Application logs (via spdlog) are automatically streamed to the device's log system and can be viewed with synapsectl logs or the Headstage GUI log viewer.

To disable log streaming (e.g., for performance-sensitive applications):

bool MyApp::setup() {
  disable_log_streaming();
  return true;
}

When log streaming is enabled (the default), log messages are published via a tap, making them accessible to any connected client.