Skip to content

Transition Trace, Telemetry & Introspection

fsmc provides rich, non-intrusive transition trace telemetry, observer callbacks, and deterministic tick-based timer management without heap memory allocations or performance overhead.


1. fsm::dispatch_result & fsm::transition_trace

Every call to dispatch(), dispatch_sync(), or post_async() returns an fsm::dispatch_result holding the dispatch status and an optional transition_trace:

namespace fsm {

enum class dispatch_status : std::uint8_t {
    success,        // Transition executed successfully
    deferred,       // Event was deferred by the active state
    guard_rejected, // Matching transition found, but guard evaluated to false
    unhandled       // No transition defined for (current_state, event)
};

enum class transition_kind : std::uint8_t {
    external,
    internal
};

struct transition_trace {
    std::string_view source{};
    std::string_view target{};
    std::string_view event{};
    std::string_view guard{};
    std::string_view action{};
    transition_kind kind{transition_kind::external};

    [[nodiscard]] constexpr bool is_internal() const noexcept;
    [[nodiscard]] constexpr bool is_external() const noexcept;
};

struct dispatch_result {
    dispatch_status status = dispatch_status::unhandled;
    std::optional<transition_trace> trace = std::nullopt;

    [[nodiscard]] constexpr bool is_success() const noexcept;
    [[nodiscard]] constexpr bool is_deferred() const noexcept;
    [[nodiscard]] constexpr bool is_guard_rejected() const noexcept;
    [[nodiscard]] constexpr bool is_unhandled() const noexcept;
    [[nodiscard]] constexpr bool is_ok() const noexcept; // success || deferred
    [[nodiscard]] constexpr std::string_view to_string() const noexcept;
};

} // namespace fsm

2. Zero-Allocation Flight Recorder Telemetry

Because all std::string_view literals in transition_trace reference static string data in the compiler's read-only data section (.rodata), inspecting and serializing traces performs zero string heap allocations:

auto res = fsm.dispatch(TakeoffCmd{});

if (res.is_success() && res.trace.has_value()) {
    std::cout << "[FLIGHT RECORDER] Transition Fired:\n"
              << "  Source: " << res.trace->source << "\n"
              << "  Target: " << res.trace->target << "\n"
              << "  Event:  " << res.trace->event  << "\n"
              << "  Guard:  " << res.trace->guard  << "\n"
              << "  Action: " << res.trace->action << "\n"
              << "  Kind:   " << to_string(res.trace->kind) << "\n";
} else if (res.is_guard_rejected()) {
    std::cerr << "[GUARD REJECTED] Takeoff rejected by guard: " 
              << (res.trace ? res.trace->guard : "Unknown") << "\n";
} else if (res.is_unhandled()) {
    std::cerr << "[UNHANDLED] Event not accepted in current state\n";
}

3. Transition Observers

You can attach a compile-time or runtime observer callback to monitor transitions globally across the system:

struct transition_info {
    std::string_view source;
    std::string_view target;
    std::string_view event;
    dispatch_status status = dispatch_status::success;
    transition_kind kind = transition_kind::external;
};

Attach an observer to the FSM instance:

fsm.set_observer([](const fsm::transition_info& info) {
    if (info.is_success()) {
        CAN_Bus_SendLog(info.source.data(), info.target.data(), info.event.data());
    }
});

4. Deterministic Tick-Based Timer Manager

In hard real-time and safety-critical embedded systems, operating system background timers (std::thread, POSIX timers) introduce non-determinism and thread scheduling jitter.

fsmc provides fsm::deterministic_timer_manager<MaxTimers>, an entirely synchronous, bounded, stack/BSS-allocated timer manager:

#include "fsm/runtime/cpp/deterministic_timer.hpp"

// Allocate fixed 16-timer manager (0 heap allocations)
fsm::deterministic_timer_manager<16> timers;

// 1. Schedule a one-shot or periodic timer
timers.start_timer(1001 /* timer_id */, 500 /* duration_ms */, false /* periodic */);

// 2. Advance time synchronously in your control loop tick
uint64_t delta_ms = 10;
timers.tick(delta_ms, [&](uint32_t expired_timer_id) {
    if (expired_timer_id == 1001) {
        // Dispatch timeout event into FSM
        fsm.dispatch(TimeoutEvent{});
    }
});