|
| 1 | +# Create Ground-Derived Channels in F Prime GDS |
| 2 | + |
| 3 | +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. |
| 4 | + |
| 5 | +This guide walks through the process of creating a basic plugin that listens to incoming telemetry and publishes it back to F Prime. |
| 6 | + |
| 7 | +> [!WARNING] |
| 8 | +> 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. |
| 9 | +
|
| 10 | +> [!NOTE] |
| 11 | +> **Prerequisite** |
| 12 | +> Before following this guide, you should first complete the [Develop GDS Plugins](../how-to/develop-gds-plugins.md) guide. |
| 13 | +> It explains the plugin system, registration process, and runtime behavior essential to this example. |
| 14 | +
|
| 15 | +--- |
| 16 | + |
| 17 | +## When to Use This |
| 18 | + |
| 19 | +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: |
| 20 | + |
| 21 | +- You want to transform or scale telemetry (e.g., converting a raw sensor ADC measurement to engineering units) |
| 22 | +- You want to compute a value based on multiple telemetry channels (e.g., battery differential) |
| 23 | +- You want to normalize or rename telemetry values for downstream consumers |
| 24 | + |
| 25 | +--- |
| 26 | + |
| 27 | +## Plugin Architecture |
| 28 | + |
| 29 | +Ground-derived channels are implemented using a `DataHandlerPlugin`, one of the built-in plugin types in the F Prime GDS. |
| 30 | + |
| 31 | +Our plugin will: |
| 32 | + |
| 33 | +- **Receive decoded telemetry data** via the `data_callback` API |
| 34 | +- **Apply your transformation logic** |
| 35 | +- **Publish the result** using the standard pipeline |
| 36 | + |
| 37 | +The plugin runs in the `CustomDataHandler` process, isolated from the core GDS. |
| 38 | + |
| 39 | +--- |
| 40 | + |
| 41 | +## Basic Setup |
| 42 | + |
| 43 | +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: |
| 44 | + |
| 45 | +```python |
| 46 | +from fprime_gds.common.handlers import DataHandlerPlugin |
| 47 | +from fprime_gds.plugin.definitions import gds_plugin |
| 48 | + |
| 49 | + |
| 50 | +@gds_plugin(DataHandlerPlugin) |
| 51 | +class ExampleGroundDerivedChannel(DataHandlerPlugin): |
| 52 | + """ Example Ground derived channel |
| 53 | + """ |
| 54 | + |
| 55 | + @classmethod |
| 56 | + def get_name(cls): |
| 57 | + """ Return the name of the plugin """ |
| 58 | + return "example-ground-derived-channel" |
| 59 | + |
| 60 | + @classmethod |
| 61 | + def get_arguments(cls): |
| 62 | + """ No special arguments""" |
| 63 | + return {} |
| 64 | + |
| 65 | + def get_handled_descriptors(self): |
| 66 | + """ List descriptors of F Prime data types that this plugin can handle """ |
| 67 | + # Inform the GDS we want to process telemetry channels |
| 68 | + return ["FW_PACKET_TELEM"] |
| 69 | + |
| 70 | + def data_callback(self, data, source): |
| 71 | + """ Handle channel objects |
| 72 | + """ |
| 73 | + pass |
| 74 | +``` |
| 75 | + |
| 76 | +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`. |
| 77 | + |
| 78 | +## Defining New Channels |
| 79 | + |
| 80 | +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: |
| 81 | + |
| 82 | +``` |
| 83 | +{ |
| 84 | + "metadata" : { |
| 85 | + "deploymentName" : "GroundChannels", |
| 86 | + "projectVersion" : "<match your project>", |
| 87 | + "frameworkVersion" : "<match your project>", |
| 88 | + "libraryVersions" : [], |
| 89 | + "dictionarySpecVersion" : "1.0.0" |
| 90 | + }, |
| 91 | + "telemetryChannels" : [ |
| 92 | + { |
| 93 | + "name" : "Examples(Ground).CommandCountMinus7", |
| 94 | + "type" : { |
| 95 | + "name" : "I64", |
| 96 | + "kind" : "integer", |
| 97 | + "size" : 64 |
| 98 | + }, |
| 99 | + "id" : 1000020224, |
| 100 | + "telemetryUpdate" : "always", |
| 101 | + "annotation" : "Output (derived) of command count less seven" |
| 102 | + } |
| 103 | + ], |
| 104 | + |
| 105 | +
|
| 106 | +
|
| 107 | + "typeDefinitions" : [], |
| 108 | + "constants" : [], |
| 109 | + "commands" : [], |
| 110 | + "parameters" : [], |
| 111 | + "events" : [], |
| 112 | + "records" : [], |
| 113 | + "containers" : [], |
| 114 | + "telemetryPacketSets" : [] |
| 115 | +} |
| 116 | +``` |
| 117 | + |
| 118 | +Make sure that the "name" and "id" fields are unique to your derived channel. |
| 119 | + |
| 120 | +## Deriving Values and Time |
| 121 | + |
| 122 | +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. |
| 123 | + |
| 124 | +```python |
| 125 | + def data_callback(self, data, source): |
| 126 | + """ Handle channel objects |
| 127 | + """ |
| 128 | + # Filter out all channels that we do not need to create our derivation |
| 129 | + if not data.template.get_full_name().endswith("CommandsDispatched"): |
| 130 | + return |
| 131 | + # Operate on the channel's `val` field |
| 132 | + new_value = data.val - 7 |
| 133 | + # Publish the new channel. |
| 134 | + self.publish_channel("Examples(Ground).CommandCountMinus7", new_value, data.time) |
| 135 | +``` |
| 136 | + |
| 137 | +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. |
| 138 | + |
| 139 | +## Running It |
| 140 | + |
| 141 | +Install the plugin as directed in the plugin development How-To. 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. |
| 142 | + |
| 143 | +``` |
| 144 | +fprime-merge-dictionary --permissive --output MergedDictionary.json \ |
| 145 | + /path/to/flight/dictionary \ |
| 146 | + ./MyGroundChannelsDictionary.json |
| 147 | +fprime-gds --dictionary ./MergedDictionary.json --no-zmq |
| 148 | +``` |
| 149 | +--- |
| 150 | + |
| 151 | +## See Also |
| 152 | + |
| 153 | +- [DataHandlerPlugin Reference](../reference/gds-plugins/data-handler.md) |
| 154 | +- [Plugin System Overview](../how-to/develop-gds-plugins.md) |
| 155 | +- [Cosine Example](https://github.com/nasa/fprime-examples/tree/devel/GdsExamples/gds-plugins/src/ground_channels) |
0 commit comments