Skip to content
Open
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
75 changes: 72 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,15 @@ as shown below). This will also produce a `<component>.template.py` file in the
implementing components in python.

Every port, command, event, telemetry channel, parameter and internal port of the component is bound, as is the
overflow hook of each member with `hook` queue-full behavior (see below). Three component features cannot be bound
yet, because F Prime declares a handler for each that has no Python equivalent:
overflow hook of each member with `hook` queue-full behavior and each instance of an internal state machine (see
below). A few component features cannot be bound yet, because F Prime declares a handler for each that has no Python
equivalent:

| Feature | Why | What happens |
| --- | --- | --- |
| serial port | its handler takes a serialization buffer | rejected with an error naming the port |
| state machine instance | its actions and guards take a state machine id and a signal | rejected with an error naming the component |
| instance of an external state machine | its actions and guards are declared by hand-written C++ (`<Machine>_Interface`), not in the model | rejected with an error naming the instance |
| state machine instance with `hook` queue-full behavior | its overflow hook takes the queued signal as a raw serial buffer | rejected with an error naming the instance |
| data product container | its handler takes a `Fw::DpContainer` | rejected with an error naming the component |

In each case the autocoder refuses rather than generating a class that would leave the handler unimplemented and
Expand Down Expand Up @@ -116,6 +118,73 @@ def DO_cmdOverflowHook(self, opCode, cmdSeq):
An exception raised in a hook is not caught by the generated C++, as for any handler (see [Strings](#strings)), but
here it reaches the caller's thread.

### State Machines

A component may instantiate internal state machines: ones whose actions, guards, signals and states are defined in
FPP. Take this one, instantiated twice:

```
module Ref {
state machine Blinker {
action store: U32
guard allow: U32
signal advance: U32
signal back
initial enter IDLE
state IDLE { on advance if allow do { store } enter BUSY }
state BUSY { on back enter IDLE }
}

@ fprime-python
queued component PySm {
...
state machine instance first: Blinker
state machine instance second: Blinker
}
}
```

Each action and guard is a Python handler, named as F Prime names it after the machine's flattened qualified name:
`Ref_Blinker_action_store(self, smId, signal, value)` and `Ref_Blinker_guard_allow(self, smId, signal, value)` for a
machine `Ref.Blinker`. Every handler takes `smId` and `signal`; one whose action or guard declares a type also takes
the signal's `value`. The handlers belong to the machine, not to an instance, so `first` and `second` call the same
ones, and `smId` says which: it is an `enum.Enum` bound inside the component's class, `fprime_py.Ref.PySm.SmId`, with
one value per instance named after it (`smId.name == "first"`). A guard is `const` in C++ and must return a `bool`;
anything else, `None` included, raises `TypeError`. The template's guard stubs return `False`, which leaves the guarded
transition untaken. Every action and guard must be implemented: a missing one raises `AttributeError` from the
generated `init`.

Python drives each instance through the methods F Prime generates for it:

- `first_sendSignal_advance(value)` and `first_sendSignal_back()` queue a signal on the component's queue. A string
value is checked first against the size F Prime serializes it at: its declared size, else
`FW_FIXED_LENGTH_STRING_SIZE`. On a full queue the instance's queue-full behavior applies: `assert` (the default)
asserts, `drop` drops the signal, and `block` waits for room with the GIL released, so the dispatching thread can run
the handlers that make it. Never send to a `block` instance from the thread that dispatches its queue (any action or
guard, or on a queued component the code that calls `doDispatch`): on a full queue that send waits forever.
- `first_getState()` returns the current state as a `fprime_py.Ref.Blinker_State`, an F Prime enumeration whose `e`
is a `Blinker_State.T`, e.g. `Blinker_State.T.IDLE`.

The machine's states and signals are bound once, into the Python module of the FPP module defining the machine, as
`Blinker_State`, an F Prime enumeration class whose values are in `Blinker_State.T`, and `Blinker_Signal`, an
`enum.Enum` of the values `signal` takes, in the order of their numbers. A machine defined inside a component
is named as F Prime names it, with the component folded in: `PySm_Local_State` and `PySm_Local_Signal` in the
component's module. Both enumerations include F Prime's internal entries, because Python can see them:
`Blinker_State.T.__FPRIME_UNINITIALIZED` is the state before the machine starts, and an action on the initial
transition receives `Blinker_Signal.__FPRIME_INITIAL_TRANSITION`. Inside a class body Python mangles such names, so
spell them `Blinker_Signal["__FPRIME_INITIAL_TRANSITION"]` there.

- **Thread.** Actions and guards run on the thread dispatching the component's queue, holding the GIL: the
component's own thread when it is active, and whatever calls `doDispatch` or `dispatchAvailableMessages` when it is
queued. `sendSignal` only queues the signal, and the transition happens when the queue is next dispatched.
- **Initial transition.** The actions and guards of the initial transition run during the component's `init`, on the
thread setting up the topology, before the component's queue is created and before its ports are connected. They
must not send signals or invoke ports; they may only set up Python state.
- **Exceptions.** An exception raised in an action or guard is not caught by the generated C++, as for any handler
(see [Strings](#strings)). One raised on the initial transition propagates out of the component's `init`.
- **Instances defined elsewhere.** A machine defined inside another component is bound only with that component, so
it is rejected; define it in a module instead.

## Setting a Deployment Up With Python Bindings

The deployment topology that instantiates Python components must also carry the `@ fprime-python` annotation. Only an
Expand Down
21 changes: 18 additions & 3 deletions src/fprime_python/autocode/binding_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
#: Doc comment placed on every generated initialization function
INIT_FUNCTION_COMMENT = """\\brief bind {fqn} into Python

This function initializes the Python bindings for the FPP type {fqn}. It should be called
This function initializes the Python bindings for {subject}. It should be called
from within the pybind11 module initialization macro at top level.

One language to rule them all, one language to find them...
Expand Down Expand Up @@ -113,6 +113,16 @@ def cpp_fqn(self) -> str:
""" The definition's C++-qualified name """
return cpp_name(self.fpp_name)

@property
def doc_name(self) -> str:
""" The name the generated initialization function's comment says it binds """
return self.cpp_fqn

@property
def doc_subject(self) -> str:
""" What the generated initialization function's comment says it binds, in a sentence """
return f"the FPP type {self.cpp_fqn}"

@property
def init_function_name(self) -> str:
""" The name of the generated initialization function
Expand Down Expand Up @@ -187,19 +197,24 @@ def document(self) -> CppDocBuilder:
function = doc.function(
self.init_function_name,
params=[("pybind11::module_&", "m")],
comment=lines(INIT_FUNCTION_COMMENT.format(fqn=self.cpp_fqn), margin=None),
comment=lines(INIT_FUNCTION_COMMENT.format(fqn=self.doc_name, subject=self.doc_subject), margin=None),
)
self.bind(function.body)
return doc

@property
def scope(self) -> str:
""" The FPP-qualified name of the scope whose Python submodule the definition is bound into """
return scope_of(self.fpp_name)

def invocation(self) -> Dict[str, str]:
""" The call the module initialization has to make to bind this definition

Returns:
A mapping of the FPP scope the definition lives in to the statement invoking its
initialization function against that scope's pybind11 submodule
"""
scope = scope_of(self.fpp_name)
scope = self.scope
submodule_variable = f"{MODULE_VARIABLE_PREFIX}{flat_name(scope)}"
return {scope: f"(void) {self.init_function_name}({submodule_variable});"}

Expand Down
Loading
Loading