Skip to content

Create Ground-Derived Channels in F Prime GDS

Ground-derived channels allow you to compute new values from incoming telemetry on the ground, using logic that runs entirely within the F Prime Ground Data System (GDS). These values can be published into the ground system as if they came from flight, enabling flexible monitoring, UI presentation, or conversions on the ground.

This guide walks through the process of creating a basic plugin that listens to incoming telemetry and publishes it back to F Prime.

Warning

Ground-derived channels are only supported when running with the --no-zmq option of the GDS. The ZeroMQ backend is fast but cannot allow arbitrary publishers.

Note

Prerequisite
Before following this guide, you should first complete the Develop GDS Plugins guide.
It explains the plugin system, registration process, and runtime behavior essential to this example.

A Cosine Example is provided to help users follow along.


When to Use This

Use a ground-derived channel when you need additional channels on the ground, but do not wish to compute nor downlink these values in-flight. For example:

  • You want to transform or scale telemetry (e.g., converting a raw sensor ADC measurement to engineering units)
  • You want to compute a value based on multiple telemetry channels (e.g., battery differential)
  • You want to normalize or rename telemetry values for downstream consumers

Plugin Architecture

Ground-derived channels are implemented using a DataHandlerPlugin, one of the built-in plugin types in the F Prime GDS. This plugin type is a FEATURE plugin that runs automatically via the fprime-gds invocation.

Our plugin will:

  • Receive decoded telemetry data via the data_callback API
  • Apply your transformation logic
  • Publish the result using the standard pipeline

The plugin runs in the CustomDataHandler process, isolated from the core GDS.


Basic Setup

To get started, create a Python file (e.g., ground_derived_channels.py) and define a plugin class that listens for telemetry. The initial plugin structure will look like this:

from fprime_gds.common.handlers import DataHandlerPlugin
from fprime_gds.plugin.definitions import gds_plugin


@gds_plugin(DataHandlerPlugin)
class ExampleGroundDerivedChannel(DataHandlerPlugin):
    """ Example Ground derived channel
    """

    @classmethod
    def get_name(cls):
        """ Return the name of the plugin """
        return "example-ground-derived-channel"

    @classmethod
    def get_arguments(cls):
        """ No special arguments"""
        return {}

    def get_handled_descriptors(self):
        """ List descriptors of F Prime data types that this plugin can handle """
        # Inform the GDS we want to process telemetry channels
        return ["FW_PACKET_TELEM"]

    def data_callback(self, data, source):
        """ Handle channel objects
        """
        pass

Tip

While plugins can be placed anywhere, a typical structure would see this file placed in a directory like plugins/src to keep plugins separated from F´ C++ code.

The two critical functions are: get_handled_descriptors, and data_callback. get_handled_descriptors returns a list of descriptors to listen to, and data_callback will give a place to perform our calculations. In our case, we only want to subscribe to telemetry channels (i.e. FW_PACKET_TELEM). When using the telemetry packetizer, users may alternatively subscribe to FW_PACKET_PACKETIZED_TLM.

Defining New Channels

To publish new channels, you must define them. Create a new dictionary file (e.g. "MyGroundChannelsDictionary.json") and add the channels you need. This structure will look like the following:

{
  "metadata" : { 
    "deploymentName" : "GroundChannels",
    "projectVersion" : "<match your project>",
    "frameworkVersion" : "<match your project>",
    "libraryVersions" : [], 
    "dictionarySpecVersion" : "1.0.0"
  },  
  "telemetryChannels" : [ 
    {   
      "name" : "Examples(Ground).CommandCountMinus7",
      "type" : { 
        "name" : "I64",
        "kind" : "integer",
        "size" : 64
      },  
      "id" : 1000020224,
      "telemetryUpdate" : "always",
      "annotation" : "Output (derived) of command count less seven"
    }   
  ],



  "typeDefinitions" : [],
  "constants" : [], 
  "commands" : [], 
  "parameters" : [], 
  "events" : [], 
  "records" : [], 
  "containers" : [], 
  "telemetryPacketSets" : []
}

Make sure that the "name" and "id" fields are unique to your derived channel.

Deriving Values and Time

Now that you have a plugin structure and dictionary, it is time to derive the channel. The sample code below produces a count from CommandsDispatched less seven.

    def data_callback(self, data, source):
        """ Handle channel objects
        """
        # Filter out all channels that we do not need to create our derivation
        if not data.template.get_full_name().endswith("CommandsDispatched"):
            return
        # Operate on the channel's `val` field
        new_value = data.get_val_obj().val - 7
        # Publish the new channel.
        self.publisher.publish_channel("Examples(Ground).CommandCountMinus7", new_value, data.time)

First this code filters out unwanted channels. Then it performs a translation on the data's value. Then it publishes the new channel supplying name, value, and time. In this case, we have reused the original time.

Running It

Install the plugin as directed in the plugin development How-To section packaging and testing plugins . Next we need to merge our dictionaries and run it. This is accomplished by running the merge dictionary command, and then supplying the output to the --dictionary flag of the GDS.

Merging Dictionaries

fprime-merge-dictionary --permissive --output MergedDictionary.json \
    /path/to/flight/dictionary \
    ./MyGroundChannelsDictionary.json

Running With The Merged Dictionary

fprime-gds --dictionary ./MergedDictionary.json --no-zmq


See Also