Skip to content

Bind internal state machines to Python - #16

Open
lestarch-autobot wants to merge 9 commits into
fprime-community:mainfrom
lestarch-autobot:devin/1793312058-state-machines
Open

lestarch-autobot wants to merge 9 commits into
fprime-community:mainfrom
lestarch-autobot:devin/1793312058-state-machines

Conversation

@lestarch-autobot

Copy link
Copy Markdown
Contributor

Summary

Python components (@ fprime-python) can now contain internal state-machine instances. An internal machine is one whose states, signals, actions and guards are all written in FPP. Each of the machine's actions and guards becomes a forwarded Python handler. Python can send signals to each instance and read its state. Before this PR, any state-machine instance was rejected.

module Ref {
  state machine Blinker { action store: U32  guard allow: U32  signal advance: U32  signal back ... }
  queued component PySm { state machine instance first: Blinker  state machine instance second: Blinker ... }
}
class PySm(PySmBase):
    def Ref_Blinker_action_store(self, smId, signal, value): ...   # one handler per machine; smId says which instance called it
    def Ref_Blinker_guard_allow(self, smId, signal, value) -> bool: return value > 1
    ...
    self.first_sendSignal_advance(4)                              # queued on the component's queue
    self.first_getState().e == fprime_py.Ref.Blinker_State.T.BUSY

Design points

  • view.py: a StateMachineHandlerView(HandlerView) base, with StateMachineActionView and StateMachineGuardView on top. These are added to ComponentView.handlers the same way as Bind hook queue-full overflow hooks to Python #15's OverflowHookView, so the generic forwarder and template loops emit them unchanged. Guards set IS_CONST and RETURNS_STRICT_BOOL: a guard is a const override, and returning a non-bool (including None) raises TypeError. The template's guard stubs return False.
  • Value spelling follows fpp-to-cpp: a value is passed by value only when its declared type is primitive. Aliases, enums, structs and strings are passed as const T&. Ports, by contrast, take aliases by value, so the port rule is deliberately not reused here.
  • Enums:
    • SmId is a native_enum bound inside the component's class.
    • <M>_State (F Prime enum wrapper) and <M>_Signal (native_enum, not exported) are bound once per machine in <M>StateMachineBindingAc.*, because pybind11 rejects a second registration.
    • Signals are bound in F Prime's numbering order.
    • F Prime's internal entries __FPRIME_INITIAL_TRANSITION and __FPRIME_UNINITIALIZED are exposed, because Python actually receives them.
  • <inst>_sendSignal_<s> checks string values against their serialized size. On block instances it releases the GIL (Python's global lock) while waiting for queue room. <inst>_getState is also bound.
  • The generated init raises AttributeError if any action or guard is missing (the same pattern as Bind hook queue-full overflow hooks to Python #15's overflow hooks). The initial-transition action runs inside init, before the queue is created and the ports are connected.
  • visitor.emit raises UnsupportedModelError when two generated files would get the same path.

Still rejected, each with an error naming the reason

  • External state machines: their actions and guards exist only in hand-written C++, so the generator has nothing to work from.
  • State-machine instances with hook: their overflow hook takes a raw FwEnumStoreType signal and a Fw::SerialBufferBase value.
  • Instances of a machine defined inside another component.
  • Data products: no change.

Docs and tests

  • README: a new state-machines section covers naming, smId, guard rules, the enums, threading (handlers run on whichever thread dispatches the queue), queue-full behavior per instance (assert/drop/block, and the block self-deadlock caveat), init-time behavior, and the uncaught-exception caveat. The unsupported-features table is updated.
  • Tests: 30 pass, up from 13. New fixtures are StateMachines.fpp, SmShared.fpp, SmExternal.fpp, SmHook.fpp, SmForeign.fpp and SmProducts.fpp. They cover signatures (including a "wide" machine with alias, enum, struct and unsized-string values, a nested state and a choice), GIL handling, the startup check, stubs, the bindings, deduplicated registration, and every rejection.

Validation

  • Strict mypy is clean, and pytest tests passes 30 of 30.
  • Generated C++ compiles: c++ -fsyntax-only of the fprime-python output for the test fixtures, against F Prime headers and pybind11, gives 0 errors in 11 of 11 files.
  • fprime-python-reference, with a temporary Probe machine on PythonGc (alias-typed value, block instance):
    • The initial transition runs the action with __FPRIME_INITIAL_TRANSITION.
    • The guard returns False for value 1, and the machine stays IDLE. For values 2 and 4 it returns True, the action receives the value, and the machine moves to BUSY.
    • getState() returns Probe_State.
    • Integration tests pass 4 of 4.
    • With the action removed, setup fails with PythonGc does not implement state machine action FprimePythonReference_Probe_action_store. The BufferManager::cleanup teardown assert that follows already happened before this PR.
  • Local review in 2 rounds (code, correctness, design, architecture, operational, docs, maintainability, tests, security), nothing posted to GitHub. Round 1 found 2 must-fix items; both are fixed with regression tests that fail when the fix is reverted. Round 2 gave Go on every lens. Its one doc item (the block self-deadlock caveat) is fixed in the last commit.
  • CI: this repository has no GitHub Actions runs at all, so every check above was run locally.

Co-authored-by: M Starch LeStarch@googlemail.com

lestarch-autobot and others added 9 commits October 29, 2026 23:04
Components with instances of internal state machines are now accepted. Each action and guard of an instantiated machine becomes a forwarded handler (guards const, returning a checked bool), missing ones are reported at init, the template stubs them (guards return False), and the instances' getState/sendSignal methods and the component's SmId are bound. External state machines and instances with 'hook' queue-full behavior are rejected with errors saying why.

Co-authored-by: M Starch <LeStarch@googlemail.com>
The module defining an internal state machine (or the annotated component a machine is nested in) binds its <Machine>_State enumeration and <Machine>StateMachineBase::Signal as <Machine>_Signal, so a machine shared by several instances or components is registered once.

Co-authored-by: M Starch <LeStarch@googlemail.com>
Fixtures with a shared internal machine (typed, untyped and string signals; two instances; two components), a machine nested in a component, a machine defined in an imported model, an external machine and a hooked instance.

Co-authored-by: M Starch <LeStarch@googlemail.com>
Add a State Machines section and update the table of unsupported features.

Co-authored-by: M Starch <LeStarch@googlemail.com>
Co-authored-by: M Starch <LeStarch@googlemail.com>
…the GIL on blocking sends

A state machine value of an alias type is passed by const reference, as F Prime
declares it, even when the alias names a primitive. Signals sent to a 'block'
instance release the GIL while waiting; unsized string signals are checked
against FW_FIXED_LENGTH_STRING_SIZE. Signals are bound in value order, action
and guard views share a base, and duplicate output paths are rejected.

Co-authored-by: M Starch <LeStarch@googlemail.com>
Co-authored-by: M Starch <LeStarch@googlemail.com>
…ehavior

Co-authored-by: M Starch <LeStarch@googlemail.com>
…tching thread

Co-authored-by: M Starch <LeStarch@googlemail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant