Loading...

SDK Overview

Overview

Synapse App SDK

This SDK provides the tools need to build your custom C++ applications for deployment on Synapse devices, such as SciFi. The SDK handles the complexities of inter-process communication, performance profiling, and configuration management, allowing you to focus on your application logic.

Synapse Apps are currently only available in C++. This documentation assumes familiarity with C++, git, and the command line.

To begin, you will need to either clone or fork the synapse-example-application repository. From there, you can begin writing your own application. Detailed setup and installation instructions can be found here.

This tutorial covers the basics of Synapse App development. After this tutorial, you should be able to:

  • Receive broadband frames in your application
  • Perform basic signal processing
  • Monitor the performance of your function calls
  • Send processed data to a client application

An example showing implementation of a Synapse App can be found here. This example will be used throughout.

App

Two files are important for application development. First, you will need a header file (.hpp) which creates a class that inherits from the Synapse App class. This is located in the /include/apps/ folder of the example application repository. Second, you will need a source file (.cpp), which contains the main source code for your application.

// synapse-example-app/include/apps/example_app.hpp
#pragma once

#include <synapse-app-sdk/app/app.hpp>
namespace app {
 class ExampleApp : public synapse::App {
  public:
    ExampleApp() = default;
    
    // We will implement any setup here
    virtual bool setup() override;
 
  protected:
    // Our main application loop
    virtual void main() override;
 };
}  // namespace app

Two core functions are required when implementing any application: setup() and main(). Your setup code will run once at startup. During runtime, your application will remain within the main() function's while loop.

The first thing to implement is the setup function. The setup function initializes any readers of broadband data, set up output data streams, or enable performance profiling.

Once objects inheriting the app class have been defined, they can be used in your application. This is done using your source file as in the example below:

// synapse-example-app/src/apps/example_app.cpp
#include "apps/example_app.hpp"

namespace app {
bool ExampleApp::setup() {
  // The set up function gets called before your program starts
  // To set up your reader for broadband data, you can specify it with the node id for the broadband data in your config
  // Replace this with your node id
  const uint32_t broadband_node_id = 1;
  if (!setup_reader(broadband_node_id) {
    spdlog::error("Failed to set up broadband reader");
    return false;
}
...

The example below initializes the App member data_reader_ to subscribe to output from the broadband node. The code then listens to and parses protobuf messages:

void ExampleApp::get_frames() {
  while (node_running_) {
    // Wait for all the data in this message sequence
    auto messages = data_reader_->receive_multipart();
    if (messages.empty()) {
        // Wait for more messages, but don't spin the CPU
        std::this_thread::sleep_for(std::chrono::microseconds(1));
        continue;
     }
     // We have messages now, we can process them and convert them to broadband frames
    for (auto& message : messages) {
      const auto broadband_frame = synapse::parse_protobuf_message<synapse::BroadbandFrame>(std::move(message)).value();
        }
}

SDK functions

Functions available through the SDK are divided into 4 types: Readers, Taps, DSPs and Utilities. These are used to read, write and process application data. The application data is read in or written out to other nodes or clients running synapsectl using readers or taps. Readers are used to read and write data within the signal chain. Taps are used to read and write data to and from external sources.

Readers

Readers acquire data from other nodes within the signal chain, such as ingesting broadband data into your application. Readers pass data between nodes, but cannot be used to port data to inputs outside the signal chain. For example, a reader could be created from the Synapse broadband node, allowing the data to be processed by your application node.

setup_reader

Create a reader from node with id node_id. Returns the status of node creation.

bool setup_reader(const uint32_t node_id);

Example:

const uint32_t broadband_node_id = 1;
    if (!setup_reader(broadband_node_id))
    {
      spdlog::warn("Failed to set up reader for controller");
      return 1;
    }

Identifies node creation status and returns a warning if reader setup fails.

Taps

Taps are ports that are used to read data from nodes to a client (e.g., a computer running synapse-python). For example, you can create a tap in your application that allows a local client to receive data over Wifi from within you application. Application data processing can be monitored by placing taps at multiple stages in the application data stream.

create_tap

Create a tap called name. Type can be specified to be any synapse datatype. Returns status.

bool create_tap(const std::string &name)

Example:

 if (!create_tap<synapse::Tensor>("joystick_out"))
    {
      spdlog::warn("Failed to create tap for joystick out");
      return false;
    }
    return true;

Creates a tap called "joystick_out" of type Tensor.

publish_tap

Publish data to a tap of a given name. Returns status.

bool publish_tap(const std::string &name, const T &message)

Example:

if (publish_tap("joystick_out", output_tensor))
        {
          spdlog::info("Published tensor: [x,y]: [{},{}]", tensor_data[0], tensor_data[1]);
        }
        else
        {
          spdlog::warn("Failed to publish tensor data");
        }

Publish Tensor data or output a warning stating publication failed.

DSP

Synapse SDK also includes several standard signal processing functions. These are fast implementations of common operations.

Filters

Returns filter instance

std::unique_ptr<synapse::BaseFilter> create_bandpass_filter<int FilterOrder>(const float sample_rate_hz, const float low_cutoff_hz,
                       const float high_cutoff_hz)

Example initialization:

const float low_cutoff_hz_ = 200.0;
const float high_cutoff_hz_ = 5000.0;
static constexpr int kSpectralFilterOrder = 2;
float sample_rate_hz_ = 32000.0;

auto filter_ptr = synapse::create_bandpass_filter<kSpectralFilterOrder>(
          sample_rate_hz, low_cutoff_hz_, high_cutoff_hz_);
      if (filter_ptr == nullptr)
      {
        spdlog::error("Failed to create filter for channel: {}", channel_index);
      }

Example execution:

const float filtered_data = filter_ptr->filter(frame_data[channel_id]);

Threshold Detection

Returns spike detector instance

std::unique_ptr<BaseSpikeDetector> create_threshold_detector(const float threshold, const uint32_t waveform_size,
                          const uint64_t refractory_us,
                          const float sample_rate_hz)

Example initialization:

const float spike_threshold_ = 50.0;         // Threshold in microvolts
const uint32_t waveform_size_ = 50;          // Total samples per waveform
const uint64_t refractory_period_us_ = 1000; // 1ms refractory period
float sample_rate_hz_ = 32000.0;            


auto detector_ptr = synapse::create_threshold_detector(spike_threshold_, waveform_size_,refractory_period_us_, sample_rate_hz_);

      if (detector_ptr == nullptr)
      {
        spdlog::error("Failed to create spike detector for channel: {}", channel_index);
      }

Example execution:

synapse::SpikeEvent *spike_event =
                detector_ptr->detect(filtered_data, frame_timestamp_ns, channel_id);

            if (spike_event != nullptr)
            {
              // Store the detected spike for further processing
              detected_spikes_.push_back(spike_event);

              // Increment the spike count for this channel
              spike_counts[channel_id]++;

Utilities

parse_protobuf_message

Parse protobuf message from within a zmq message. Returns parsed message as type T.

std::optional<T> parse_protobuf_message(zmq::message_t message)

Example:

frames.reserve(frames.size() + messages.size());

      // Process each received message in this multipart
      for (auto &message : messages)
      {
        // Parse the message into a BroadbandFrame
        const auto maybe_frame =
synapse::parse_protobuf_message<synapse::BroadbandFrame>(std::move(message));
        if (!maybe_frame.has_value())
        {
          spdlog::warn("Failed to parse broadband frame");
          // If we have no frames at all, return false
          if (frames.empty())
          {
            return false;
          }
          // Otherwise, return what we have so far
          return true;
        }

        const auto &broadband_frame = maybe_frame.value();