Skip to main content

Fpy Advanced Sequencing Language for F Prime

Project description

Fpy User's Guide

Fpy is an easy to learn, powerful spacecraft scripting language backed by decades of JPL heritage. It is designed to work with the FPrime flight software framework. The syntax is inspired by Python, and it compiles to an efficient binary format.

This guide is a quick overview of the most important features of Fpy. It should be easy to follow for someone who has used Python and FPrime before.

1. Compiling and Running a Sequence

First, make sure fprime-fpy is installed.

Fpy sequences are suffixed with .fpy. Let's make a test sequence that dispatches a no-op:

# hash denotes a comment
# assume this file is named "test.fpy"

# use the full name of the no-op command:
CdhCore.cmdDisp.CMD_NO_OP() # empty parentheses indicate no arguments

You can compile it with fprime-fpyc test.fpy --dictionary Ref/build-artifacts/Linux/dict/RefTopologyDictionary.json

Make sure your deployment topology has an instance of the Svc.FpySequencer component. You can run the sequence by passing it in as an argument to the Svc.FpySequencer.RUN command.

2. Variables and Basic Types

Fpy supports statically-typed, mutable local variables. You can change their value, but the type of the variable can't change.

This is how you declare a variable, and change its value:

unsigned_var: U8 = 0
# this is a variable named unsigned_var with a type of unsigned 8-bit integer and a value of 0

unsigned_var = 123
# now it has a value of 123

For types, Fpy has most of the same basic ones that FPP does:

  • Signed integers: I8, I16, I32, I64
  • Unsigned integers: U8, U16, U32, U64
  • Floats: F32, F64
  • Boolean: bool
  • Time: Fw.Time

Float literals can include either a decimal point or exponent notation (5.0, .1, 1e-5), and Boolean literals have a capitalized first letter: True, False. There is no way to differentiate between signed and unsigned integer literals.

Note there is currently no built-in string type. See Strings.

3. Type coercion and casting

If you have a lower-bitwidth numerical type and want to turn it into a higher-bitwidth type, this happens automatically:

low_bitwidth_int: U8 = 123
high_bitwidth_int: U32 = low_bitwidth_int
# high_bitwidth_int == 123
low_bitwidth_float: F32 = 123.0
high_bitwidth_float: F64 = low_bitwidth_float
# high_bitwidth_float == 123.0

However, the opposite produces a compile error:

high_bitwidth: U32 = 25565
low_bitwidth: U8 = high_bitwidth # compile error

If you are sure you want to do this, you can manually cast the type to the lower-bitwidth type:

high_bitwidth: U32 = 16383
low_bitwidth: U8 = U8(high_bitwidth) # no more error!
# low_bitwidth == 255

This is called downcasting. It has the following behavior:

  • 64-bit floats are downcasted to 32-bit floats as if by static_cast<F32>(f64_value) in C++
  • Unsigned integers are bitwise truncated to the desired length
  • Signed integers are first reinterpreted bitwise as unsigned, then truncated to the desired length. Then, if the sign bit of the resulting number is set, 2 ** dest_type_bits is subtracted from the resulting number to make it negative. This may have unintended behavior so use it cautiously.

You can turn an int into a float implicitly:

int_value: U8 = 123
float_value: F32 = int_value

But the opposite produces a compile error:

float_value: F32 = 123.0
int_value: U8 = float_value # compile error

Instead, you have to manually cast:

float_value: F32 = 123.0
int_value: U8 = U8(float_value)
# int_value == 123

In addition, you have to cast between signed/unsigned ints:

uint: U32 = 123123
int: I32 = uint # compile error
int: I32 = I32(uint)
# int == 123123

4. Dictionary Types

Fpy also has access to all structs, arrays and enums in the FPrime dictionary:

# you can access enum constants by name:
enum_var: Fw.Success = Fw.Success.SUCCESS

# you can construct arrays:
array_var: Ref.DpDemo.U32Array = Ref.DpDemo.U32Array(0, 1, 2, 3, 4)

# you can construct structs:
struct_var: Ref.SignalPair = Ref.SignalPair(0.0, 1.0)

In general, the syntax for instantiating a struct or array type is Full.Type.Name(arg, ..., arg).

5. Math

You can do basic math and store the result in variables in Fpy:

pemdas: F32 = 1 - 2 + 3 * 4 + 10 / 5 * 2 # == 15.0

Fpy supports the following math operations:

  • Basic arithmetic: +, -, *, /
  • Modulo: %
  • Exponentiation: **
  • Floor division: //
  • Natural logarithm: log(F64)
  • Absolute value: fabs(F64), iabs(I64)

The behavior of these operators is designed to mimic Python.

Note that division always returns a float. This means that 5 / 2 == 2.5, not 2. This may be confusing coming from C++, but it is consistent with Python. If you want integer division, use the // operator.

6. Variable Arguments to Commands, Macros and Constructors

Where this really gets interesting is when you pass variables or expressions into commands:

# this is a command that takes an F32
Ref.sendBuffComp.PARAMETER4_PRM_SET(1 - 2 + 3 * 4 + 10 / 5 * 2)
# alternatively:
param4: F32 = 15.0
Ref.sendBuffComp.PARAMETER4_PRM_SET(param4)

You can also pass variable arguments to the sleep, exit, fabs, iabs and log macros, as well as to constructors.

There are some restrictions on using string values, or complex types containing string values. See Strings.

7. Getting Telemetry Channels and Parameters

Fpy supports getting the value of telemetry channels:

cmds_dispatched: U32 = CdhCore.cmdDisp.CommandsDispatched

signal_pair: Ref.SignalPair = Ref.SG1.PairOutput

It's important to note that if your component hasn't written telemetry to the telemetry database (TlmPacketizer or TlmChan) in a while, the value the sequence sees may be old. Make sure to regularly write your telemetry!

Fpy supports getting the value of parameters:

prm_3: U8 = Ref.sendBuffComp.parameter3

A significant limitation of this is that it will only return the value most recently saved to the parameter database. This means you must command _PRM_SAVE before the sequence will see the new value.

Note: If a telemetry channel and parameter have the same fully-qualified name, the fully-qualified name will get the value of the telemetry channel

8. Conditionals

Fpy supports comparison operators:

value: bool = 1 > 2 and (3 + 4) != 5
  • Inequalities: >, <, >=, <=
  • Equalities: ==, !=
  • Boolean functions: and, or, not

Boolean and and or short-circuit just like Python: the right-hand expression only evaluates when the result is still undecided.

The inequality operators can compare two numbers of any type together. The equality operators, in addition to comparing numbers, can check for equality between two of the same complex type:

record1: Svc.DpRecord = Svc.DpRecord(0, 1, 2, 3, 4, 5, Fw.DpState.UNTRANSMITTED)
record2: Svc.DpRecord = Svc.DpRecord(0, 1, 2, 3, 4, 5, Fw.DpState.UNTRANSMITTED)
records_equal: bool = record1 == record2 # == True

9. If/elif/else

You can branch off of conditionals with if, elif and else:

random_value: I8 = 4 # chosen by fair dice roll. guaranteed to be random

if random_value < 0:
    CdhCore.cmdDisp.CMD_NO_OP_STRING("won't happen")
elif random_value > 0 and random_value <= 6:
    CdhCore.cmdDisp.CMD_NO_OP_STRING("should happen!")
else:
    CdhCore.cmdDisp.CMD_NO_OP_STRING("uh oh...")

This is particularly useful for checking telemetry channel values:

# dispatch a no-op
CdhCore.cmdDisp.CMD_NO_OP()
# the commands dispatched count should be >= 1
if CdhCore.cmdDisp.CommandsDispatched >= 1:
    CdhCore.cmdDisp.CMD_NO_OP_STRING("should happen")

10. Check statement

A check statement is like an if, but its condition has to hold true (or "persist") for some amount of time.

check CdhCore.cmdDisp.CommandsDispatched > 30 persist Fw.TimeIntervalValue(15, 0):
    CdhCore.cmdDisp.CMD_NO_OP_STRING("more than 30 commands for 15 seconds!")

If you don't specify a value for persist, the condition only has to be true once.

You can specify an absolute time at which the check should time out:

check CdhCore.cmdDisp.CommandsDispatched > 30 timeout now() + Fw.TimeIntervalValue(60, 0) persist Fw.TimeIntervalValue(2, 0):
    CdhCore.cmdDisp.CMD_NO_OP_STRING("more than 30 commands for 2 seconds!")

You can also specify a timeout clause, which executes if the check times out:

check CdhCore.cmdDisp.CommandsDispatched > 30 timeout now() + Fw.TimeIntervalValue(60, 0) persist Fw.TimeIntervalValue(2, 0):
    CdhCore.cmdDisp.CMD_NO_OP_STRING("more than 30 commands for 2 seconds!")
timeout:
    CdhCore.cmdDisp.CMD_NO_OP_STRING("took more than 60 seconds :(")

Finally, you can specify a freq at which the condition should be checked:

check CdhCore.cmdDisp.CommandsDispatched > 30 freq Fw.TimeIntervalValue(1, 0): # check every 1 second
    CdhCore.cmdDisp.CMD_NO_OP_STRING("more than 30 commands!")

If you don't specify a value for freq, the default frequency is 1 Hertz.

The timeout, persist and freq clauses can appear in any order. They can also be spread across multiple lines:

check CdhCore.cmdDisp.CommandsDispatched > 30
    timeout now() + Fw.TimeIntervalValue(60, 0)
    persist Fw.TimeIntervalValue(2, 0)
    freq Fw.TimeIntervalValue(1, 0):
    CdhCore.cmdDisp.CMD_NO_OP_STRING("more than 30 commands for 2 seconds!")
timeout:
    CdhCore.cmdDisp.CMD_NO_OP_STRING("took more than 60 seconds :(")

11. Getting Struct Members and Array Items

You can access members of structs by name, or array elements by index:

# access struct members with "." syntax
signal_pair_time: F32 = Ref.SG1.PairOutput.time

# access array elements with "[]" syntax
com_queue_depth_0: U32 = ComCcsds.comQueue.comQueueDepth[0]

You can also reassign struct members or array elements:

# Ref.SignalPair is a struct type
signal_pair: Ref.SignalPair = Ref.SG1.PairOutput
signal_pair.time = 0.2

# Svc.ComQueueDepth is an array type
com_queue_depth: Svc.ComQueueDepth = ComCcsds.comQueue.comQueueDepth
com_queue_depth[0] = 1

12. For and while loops

You can loop while a condition is true:

counter: U64 = 0
while counter < 100:
    counter = counter + 1

# counter == 100

Keep in mind, a busy-loop will eat up the whole thread of the Svc.FpySequencer component. If you do this for long enough, the queue will fill up and the component will assert. You may want to include at least one sleep in such a loop:

while True:
    # this will execute one loop body every time checkTimers is called
    sleep()

You can also loop over a range of integers:

sum: I64 = 0
# loop i from 0 inclusive to 5 exclusive
for i in 0..5:
    sum = sum + i

# sum == 10

The loop variable, in this case i, is always of type I64. If a variable with the same name as the loop variable already exists, it can be reused as long as it is an I64:

i: I64 = 123
for i in 0..5: # okay: reuse of `i`
    sum = sum + i

There is currently no support for a step size other than 1.

While inside of a loop, you can break out of the loop:

counter: U64 = 0
while True:
    counter = counter + 1
    if counter == 100:
        break

# counter == 100

You can also continue on to the next iteration of the loop, skipping the remainder of the loop body:

odd_numbers_sum: I64 = 0
for i in 0..10:
    if i % 2 == 0:
        continue
    odd_numbers_sum = odd_numbers_sum + i

# odd_numbers_sum == 25

13. Functions

You can define and call functions:

def foobar():
    if 1 + 2 == 3:
        CdhCore.cmdDisp.CMD_NO_OP_STRING("foo")

foobar()

Functions can have arguments and return types:

def add_vals(a: U64, b: U64) -> U64:
    return a + b
    
assert add_vals(1, 2) == 3

Functions can have default argument values:

def greet(times: I64 = 3):
    for i in 0..times:
        CdhCore.cmdDisp.CMD_NO_OP_STRING("hello")

greet()  # uses default: prints 3 times
greet(1) # prints once

Default values must be constant expressions (literals, enum constants, type constructors with const args, etc.). You can't use telemetry, variables, or function calls as defaults.

Functions can access top-level variables:

counter: I64 = 0

def increment():
    counter = counter + 1

increment()
increment()
assert counter == 2

Functions can call each other or themselves:

def recurse(limit: U64):
    if limit == 0:
        return
    CdhCore.cmdDisp.CMD_NO_OP_STRING("tick")
    recurse(limit - 1)

recurse(5) # prints "tick" 5 times

Functions can only be defined at the top level—not inside loops, conditionals, or other functions.

14. Relative and Absolute Sleep

You can pause the execution of a sequence for a relative duration, or until an absolute time:

CdhCore.cmdDisp.CMD_NO_OP_STRING("second 0")
# sleep for 1 second
sleep(1)
CdhCore.cmdDisp.CMD_NO_OP_STRING("second 1")
# sleep for half a second
sleep(useconds=500_000)

# sleep until the next checkTimers call on the Svc.FpySequencer component
sleep()
CdhCore.cmdDisp.CMD_NO_OP_STRING("checkTimers called!")

CdhCore.cmdDisp.CMD_NO_OP_STRING("today")
# sleep until 1234567890 seconds and 0 microseconds after the epoch
# time base of 0, time context of 1
sleep_until(Fw.Time(0, 1, 1234567890, 0))
CdhCore.cmdDisp.CMD_NO_OP_STRING("much later")

You can also use the time() function to parse ISO 8601 timestamps:

# Parse an ISO 8601 timestamp (UTC with Z suffix)
sleep_until(time("2025-12-19T14:30:00Z"))

# With microseconds
t: Fw.Time = time("2025-12-19T14:30:00.123456Z")
sleep_until(t)

# Customize time_base and time_context (defaults are 0)
t: Fw.Time = time("2025-12-19T14:30:00Z", time_base=2, time_context=1)

Make sure that the Svc.FpySequencer.checkTimers port is connected to a rate group. The sequencer only checks if a sleep is done when the port is called, so the more frequently you call it, the more accurate the wakeup time.

15. Time Functions

Fpy provides built-in functions and operators for working with Fw.Time and Fw.TimeIntervalValue types.

You can get the current time with now():

current_time: Fw.Time = now()

The underlying implementation of now() just calls the getTime port on the FpySequencer component.

You can compare two Fw.Time values with comparison operators:

t1: Fw.Time = now()
sleep(1, 0)
t2: Fw.Time = now()

assert t1 <= t2

If the times are incomparable due to having different time bases, the sequence will assert. To safely compare times which may have different time bases, use the time_cmp function, in time.fpy.

You can also compare two Fw.TimeIntervalValue values:

interval1: Fw.TimeIntervalValue = Fw.TimeIntervalValue(5, 0)
interval2: Fw.TimeIntervalValue = Fw.TimeIntervalValue(10, 0)

assert interval1 < interval2

You can add a Fw.TimeIntervalValue to a Fw.Time:

current: Fw.Time = Fw.Time(1, 0, 100, 500000) # time base 1, context 0, 100.5 seconds
offset: Fw.TimeIntervalValue = Fw.TimeIntervalValue(60, 0) # 60 seconds
assert (current + offset).seconds == 160

You can subtract two Fw.Time values to get a Fw.TimeIntervalValue:

start: Fw.Time = Fw.Time(1, 0, 100, 0)
end: Fw.Time = Fw.Time(1, 0, 105, 500000)
assert (end - start).seconds == 5

Subtraction of two Fw.Time values asserts that both times have the same time base and that the first argument is greater than or equal to the second. If these conditions are not met, the sequence will exit with an error.

If at any point the output value would overflow, the sequence will exit with an error. Under the hood, these operators are just calling the built in time_cmp, time_sub, time_add, etc. functions in time.fpy.

16. Exit Macro

You can end the execution of the sequence early by calling the exit macro:

# exit takes a U8 argument
# 0 is the error code meaning "no error"
exit(0)
# anything else means an error occurred, and will show up in telemetry
exit(123)

17. Assertions

You can assert that a Boolean condition is true:

# won't end the sequence
assert 1 > 0
# will end the sequence
assert 0 > 1

You can also specify an error code to be raised if the expression is not true:

# will raise an error code of 123
assert 1 > 2, 123

18. Strings

Fpy does not support a fully-fledged string type yet. You can pass a string literal as an argument to a command, but you cannot pass a string from a telemetry channel. You also cannot store a string in a variable, or perform any string manipulation, or use any types anywhere which have strings as members or elements. This is due to FPrime strings using a dynamic amount of memory. These features will be added in a later Fpy update.

Fpy Developer's Guide

Workflow

  1. Make a venv
  2. pip install -e .
  3. Make changes to the source
  4. pytest

Running on a test F-Prime deployment

  1. git clone git@github.com:zimri-leisher/fprime-fpy-testbed
  2. cd fprime-fpy-testbed
  3. git submodule update --init --recursive
  4. Make a venv, install fprime requirements
  5. cd Ref
  6. fprime-util generate -f
  7. fprime-util build -j16
  8. fprime-gds. You should see a green circle in the top right.
  9. In the fpy repo, pytest --use-gds --dictionary test/fpy/RefTopologyDictionary.json test/fpy/test_seqs.py will run all of the test sequences against the live GDS deployment.

Tools

fprime-fpyc debugging flags

The compiler has an optional debug flag. When passed, the compiler will print a stack trace of where each compile error is generated.

The compiler has an optional bytecode flag. When passed, the compiler will output human-readable .fpybc files instead of .bin files.

fprime-fpy-model

fprime-fpy-model is a Python model of the FpySequencer runtime.

  • Given a sequence binary file, it deserializes and runs the sequence as if it were running on a real FpySequencer.
  • Commands always return successfully, without blocking.
  • Telemetry and parameter access always raise (PR|TL)M_CHAN_NOT_FOUND.
  • Use --debug to print each directive and the stack as it executes.

fprime-fpy-asm

fprime-fpy-asm assembles human-readable .fpybc bytecode files into binary .bin files.

fprime-fpy-disasm

fprime-fpy-disasm disassembles binary .bin files into human-readable .fpybc bytecode.

Running tests

Use pytest to run the test suite:

pytest test/

By default, debug output from the sequencer model is disabled for performance. To enable verbose debug output (prints each directive and stack state), use the --fpy-debug flag:

pytest test/ --fpy-debug

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

fprime_fpy-0.3.2.tar.gz (187.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

fprime_fpy-0.3.2-py3-none-any.whl (103.0 kB view details)

Uploaded Python 3

File details

Details for the file fprime_fpy-0.3.2.tar.gz.

File metadata

  • Download URL: fprime_fpy-0.3.2.tar.gz
  • Upload date:
  • Size: 187.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for fprime_fpy-0.3.2.tar.gz
Algorithm Hash digest
SHA256 1fb921d4d03cf19cbed81d5862808918e248568b27fa1c307700aa406a8c7b25
MD5 b9222aee2e6c9e55304ee90dc72b9ebc
BLAKE2b-256 1aad71f7eca2198d521ce8cd9cd9f00e2e050109d326d79f8c44ecdb3e10681a

See more details on using hashes here.

Provenance

The following attestation bundles were made for fprime_fpy-0.3.2.tar.gz:

Publisher: publish.yml on fprime-community/fpy

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file fprime_fpy-0.3.2-py3-none-any.whl.

File metadata

  • Download URL: fprime_fpy-0.3.2-py3-none-any.whl
  • Upload date:
  • Size: 103.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for fprime_fpy-0.3.2-py3-none-any.whl
Algorithm Hash digest
SHA256 42248a4f461d2bc1327bf1985a57e1c4e10dfc6e08088796ac204c3cf0ed1795
MD5 47ba4157d83fe13de59b88b8f1037271
BLAKE2b-256 1c7e3e81e5c6d7cf654c1509090bb9f33b6f3cbfbe7b332e5377fc41482aa7ca

See more details on using hashes here.

Provenance

The following attestation bundles were made for fprime_fpy-0.3.2-py3-none-any.whl:

Publisher: publish.yml on fprime-community/fpy

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page