Skip to content

Add processor documentation and CLI support #2108

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Jun 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions .github/workflows/cli.yml
Original file line number Diff line number Diff line change
Expand Up @@ -97,12 +97,18 @@ jobs:
ls targets/MY_CLI/lib | grep upcase_conversion.rb
../openc3.sh cli rake build VERSION=1.0.3
../openc3.sh cli validate openc3-cosmos-cli-test-1.0.3.gem
### PROCESSOR ###
../openc3.sh cli generate processor MY_CLI slope --ruby
# Verify the conversion filename 'slope_processor.rb'
ls targets/MY_CLI/lib | grep slope_processor.rb
../openc3.sh cli rake build VERSION=1.0.4
../openc3.sh cli validate openc3-cosmos-cli-test-1.0.4.gem
### LIMITS_RESPONSE ###
../openc3.sh cli generate limits_response MY_CLI example --ruby
# Verify the conversion filename 'example_limits_response.rb'
ls targets/MY_CLI/lib | grep example_limits_response.rb
../openc3.sh cli rake build VERSION=1.0.3
../openc3.sh cli validate openc3-cosmos-cli-test-1.0.3.gem
../openc3.sh cli rake build VERSION=1.0.5
../openc3.sh cli validate openc3-cosmos-cli-test-1.0.5.gem
- name: openc3.sh cli script list, run, spawn
shell: 'script -q -e -c "bash {0}"'
run: |
Expand Down
8 changes: 5 additions & 3 deletions docs.openc3.com/docs/configuration/_conversions.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ To use the conversion add the following to a telemetry item:
READ_CONVERSION double_conversion.py
```

To create a Ruby conversion simply replace `--python` with `--ruby`. This creates a conversion called `double_conversion.py` at `targets/GSE/lib/double_conversion.py`. The code which is generated looks like the following:
Note: To create a Ruby conversion simply replace `--python` with `--ruby`.

The above command creates a conversion called `double_conversion.py` at `targets/GSE/lib/double_conversion.py`. The code which is generated looks like the following:

```python
from openc3.conversions.conversion import Conversion
Expand Down Expand Up @@ -95,13 +97,13 @@ class DoubleConversion(Conversion):

### Apply Conversion

Now that we have implemented the conversion logic we need to apply it to a telemetry item by adding the line `READ_CONVERSION double_conversion.rb` in the [telemetry](/docs/configuration/telemetry) definition file. This could look something like this:
Now that we have implemented the conversion logic we need to apply it to a telemetry item by adding the line `READ_CONVERSION double_conversion.py` in the [telemetry](/docs/configuration/telemetry) definition file. This could look something like this:

```bash
TELEMETRY GSE DATA BIG_ENDIAN "Data packet"
... # Header items
APPEND_ITEM VALUE 16 UINT "Value I want to double"
READ_CONVERSION double_conversion.rb
READ_CONVERSION double_conversion.py
```

# Built-in Conversions
Expand Down
99 changes: 99 additions & 0 deletions docs.openc3.com/docs/configuration/_processors.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
---
sidebar_position: 10
title: Processors
description: Processors execute code every time a packet is received to calculate values
sidebar_custom_props:
myEmoji: 🧮
---

<!-- Be sure to edit _processors.md because processors.md is a generated file -->

# Overview

Processors execute code every time a packet is received to calculate values that can be retrieved by a [ProcessorConversion](/docs/configuration/conversions#processor_conversion). Processors are applied using the [PROCESSOR](/docs/configuration/telemetry#processor) keyword and generate values unique to the processor.

If you only want to perform calculations using a single packet to modify a telemetry value you probably want to use a [Conversion](/docs/configuration/conversions). Processors are used when you're deriving a number of values from a single telemetry item.

## Custom Processors

You can easily create your own custom processors by using the [Processor Code Generator](/docs/getting-started/generators#processor-generator). To generate a process you must be inside an existing COSMOS plugin. The generator takes both a target name and the processor name. For example if your plugin is called `openc3-cosmos-gse` and you have an existing target named `GSE`:

```bash
openc3-cosmos-gse % openc3.sh cli generate processor GSE slope --python
Processor targets/GSE/lib/slope_processor.py successfully generated!
To use the processor add the following to a telemetry packet:
PROCESSOR SLOPE slope_processor.py <PARAMS...>
```

Note: To create a Ruby processor simply replace `--python` with `--ruby`.

The above command creates a processor called `slope_processor.py` at `targets/GSE/lib/slope_processor.py`. The code which is generated looks like the following:

```python
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be Ruby to match the rest of our docs?

Copy link
Member Author

@jmthomas jmthomas Jun 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the majority of our users are going to be Python going forward so I'm slowing switching the docs over to Python as the primary example.

import math
from openc3.processors.processor import Processor

# Custom processor class
# See https://docs.openc3.com/docs/configuration/processors
class SlopeProcessor(Processor):
def __init__(self, item_name, num_samples, value_type='CONVERTED'):
super().__init__(value_type)
self.item_name = item_name.upper()
self.num_samples = int(num_samples)
self.reset()

def call(self, value, packet, buffer):
value = packet.read(self.item_name, self.value_type, buffer)
# Don't process NaN or Infinite values
if math.isnan(value) or math.isinf(value):
return

self.samples.append(value)
if len(self.samples) > self.num_samples:
self.samples = self.samples[-self.num_samples :]

if len(self.samples) > 1:
self.results['RATE_OF_CHANGE'] = (self.samples[-1] - self.samples[0]) / (len(self.samples) - 1)
else:
self.results['RATE_OF_CHANGE'] = None

def reset(self):
self.samples = []
self.results['RATE_OF_CHANGE'] = None
```

### **init**

The **init** method is where the processor is initialized. The parameters specified are the parameters given in the configuration file when creating the processor. So for our example, the telemetry configuration file will look like:

```
# Calculate the slope of TEMP1 over the last 60 samples (1 minute)
PROCESSOR SLOPE slope_processor.py TEMP1 60
```

### call

The call method is where the actual processor logic is implemented. In our case we want to calculate the rate of change from the first sample to the last sample. There are certainly more efficient ways to calculate a single rate of change value (you really only need 2 values) but this example shows how to keep a running list of values. Also note that if you're only performing a single calculation you might be better off using a [Conversion](/docs/configuration/conversions).

### reset

The reset method initializes the samples and clears any state by setting the results to `None`.

### Instantiate Processor

Now that we have implemented the processor logic we need to create the processor by adding it to a telemetry packet with the line `PROCESSOR SLOPE slope_processor.py` in the [telemetry](/docs/configuration/telemetry) definition file. We also need a [ProcessorConversion](/docs/configuration/conversions#processor_conversion) to pull the calculated values out of the processor and into a [derived](/docs/configuration/telemetry#derived-items) telemetry item. This could look something like this:

```bash
TELEMETRY GSE DATA BIG_ENDIAN "Data packet"
... # Telemetry items
ITEM TEMP1SLOPE 0 0 DERIVED "Rate of change for the last 60 samples of TEMP1"
READ_CONVERSION openc3/conversions/processor_conversion.py SLOPE RATE_OF_CHANGE
# Calculate the slope of TEMP1 over the last 60 samples (1 minute)
PROCESSOR SLOPE slope_processor.py TEMP1 60
```

If you have multiple values you're calculating you simply add additional ITEMs with READ_COVERSIONs and read the various values the processor calculates in the results.

# Built-in Processors

COSMOS_META
4 changes: 3 additions & 1 deletion docs.openc3.com/docs/configuration/_table.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
---
sidebar_position: 10
sidebar_position: 12
title: Tables
description: Table definition file format and keywords
sidebar_custom_props:
myEmoji: 🪑
---

<!-- Be sure to edit _table.md because table.md is a generated file -->
Expand Down
2 changes: 1 addition & 1 deletion docs.openc3.com/docs/configuration/_telemetry-screens.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 11
sidebar_position: 13
title: Screens
description: Telemetry Viewer screen definition and widget documentation
sidebar_custom_props:
Expand Down
11 changes: 7 additions & 4 deletions docs.openc3.com/docs/configuration/conversions.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ To use the conversion add the following to a telemetry item:
READ_CONVERSION double_conversion.py
```

To create a Ruby conversion simply replace `--python` with `--ruby`. This creates a conversion called `double_conversion.py` at `targets/GSE/lib/double_conversion.py`. The code which is generated looks like the following:
Note: To create a Ruby conversion simply replace `--python` with `--ruby`.

The above command creates a conversion called `double_conversion.py` at `targets/GSE/lib/double_conversion.py`. The code which is generated looks like the following:

```python
from openc3.conversions.conversion import Conversion
Expand Down Expand Up @@ -95,13 +97,13 @@ class DoubleConversion(Conversion):

### Apply Conversion

Now that we have implemented the conversion logic we need to apply it to a telemetry item by adding the line `READ_CONVERSION double_conversion.rb` in the [telemetry](/docs/configuration/telemetry) definition file. This could look something like this:
Now that we have implemented the conversion logic we need to apply it to a telemetry item by adding the line `READ_CONVERSION double_conversion.py` in the [telemetry](/docs/configuration/telemetry) definition file. This could look something like this:

```bash
TELEMETRY GSE DATA BIG_ENDIAN "Data packet"
... # Header items
APPEND_ITEM VALUE 16 UINT "Value I want to double"
READ_CONVERSION double_conversion.rb
READ_CONVERSION double_conversion.py
```

# Built-in Conversions
Expand Down Expand Up @@ -282,6 +284,7 @@ POLY_WRITE_CONVERSION 10 0.5 0.25

This command reads a value from a processor. The value is read from the
processor's available values. The processor must be defined in the target's configuration.
See the [Processor](/docs/configuration/processors) documentation for more information.


| Parameter | Description | Required |
Expand All @@ -298,7 +301,7 @@ ITEM TEMP1HIGH 0 0 DERIVED "High-water mark for TEMP1"

Python Example:
```python
PROCESSOR TEMP1WATER watermark_processor.rb TEMP1
PROCESSOR TEMP1WATER openc3/conversions/watermark_processor.py TEMP1
ITEM TEMP1HIGH 0 0 DERIVED "High-water mark for TEMP1"
READ_CONVERSION openc3/conversions/processor_conversion.py TEMP1WATER HIGH_WATER
```
Expand Down
96 changes: 96 additions & 0 deletions docs.openc3.com/docs/configuration/limits-response.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
---
sidebar_position: 11
title: Limits Response
description: Custom code invoked when a item with limits changes state
sidebar_custom_props:
myEmoji: ⚠️
---

# Overview

A limits response is custom code which can respond to a telemetry item changing limits states (red, yellow, green or blue). To apply a limits response to a telemetry item you use the [LIMITS_RESPONSE](/docs/configuration/telemetry#limits_response) keyword.

## Creating a Limits Response

You can easily create a limits response by using the [Limits Response Code Generator](/docs/getting-started/generators#limits-response-generator). To generate a limits response you must be inside an existing COSMOS plugin. The generator takes both a target name and the limits response name. For example if your plugin is called `openc3-cosmos-gse` and you have an existing target named `GSE`:

```bash
openc3-cosmos-gse % openc3.sh cli generate limits_response GSE abort --python
Limits response targets/GSE/lib/abort_limits_response.py successfully generated!
To use the limits response add the following to a telemetry item:
LIMITS_RESPONSE abort_limits_response.py
```

Note: To create a Ruby conversion simply replace `--python` with `--ruby`

This creates a limits response called `abort_limits_response.py` at `targets/GSE/lib/abort_limits_response.py`. The code which is generated looks like the following:

```python
from openc3.packets.limits_response import LimitsResponse
from openc3.api import *

class AbortLimitsResponse(LimitsResponse):
# @param packet [Packet] Packet the limits response is assigned to
# @param item [PacketItem] PacketItem the limits response is assigned to
# @param old_limits_state [Symbol] Previous value of the limit. One of nil,
# "GREEN_HIGH", "GREEN_LOW", "YELLOW", "YELLOW_HIGH", "YELLOW_LOW",
# "RED", "RED_HIGH", "RED_LOW". nil if the previous limit state has not yet
# been established.
def call(self, packet, item, old_limits_state):
# Take action based on the current limits state
# Delete any of the case lines that do not apply or you don't care about
match item.limits.state:
case "RED_HIGH":
# Take action like sending a command:
# cmd("TARGET SAFE")
pass
case "RED_LOW":
pass
case "YELLOW_LOW":
pass
case "YELLOW_HIGH":
pass
# GREEN limits are only available if a telemetry item has them defined
# COSMOS refers to these as "operational limits"
# See https://docs.openc3.com/docs/configuration/telemetry#limits
case "GREEN_LOW":
pass
case "GREEN_HIGH":
pass
# :RED and :YELLOW limits are triggered for STATES with defined RED and YELLOW states
# See https://docs.openc3.com/docs/configuration/telemetry#state
case "RED":
pass
case "YELLOW":
pass
```

There are a lot of comments to help you know what to do. The only thing you need to modify is the `call` method.

### call

The call method is where the limits response logic is implemented. As an example, suppose we want to send the `INST ABORT` command every time we enter a RED_HIGH or RED_LOW state. The final result with comments removed looks like the following:

```python
from openc3.packets.limits_response import LimitsResponse
from openc3.api import *
class AbortLimitsResponse(LimitsResponse):
def call(self, packet, item, old_limits_state):
match item.limits.state:
case "RED_HIGH" | "RED_LOW":
cmd("INST ABORT")
```

### Apply Conversion

Now that we have implemented the limits response logic we need to apply it to a telemetry item by adding the line `LIMITS_RESPONSE abort_limits_response.py` in the [telemetry](/docs/configuration/telemetry) definition file. This could look something like this:

```bash
TELEMETRY GSE DATA BIG_ENDIAN "Data packet"
... # Header items
APPEND_ITEM VALUE 16 UINT "limits response item"
LIMITS DEFAULT 1 ENABLED -90 -80 80 90
LIMITS_RESPONSE abort_limits_response.py
```

The definition combined with the `AbortLimitsResponse` means that each time the `GSE DATA VALUE` item goes below -90 or above 90 the `INST ABORT` command will be sent.
Loading
Loading