# Node Reference This file contains detailed per-node documentation for `@wz2b/node-red-dfsm`. ## dfsm-state-machine Defines an FSM instance and acts as the authoritative central runtime owner of machine behavior. ### What it retains - `currentState` - `previousState` - shared `context` object - allowed state list - initial state - initial context clone - monotonically increasing `eventId` ### Configuration - **Name**: optional label for the FSM instance - **Allowed states**: ordered editable list, one state name per entry - **Initial state**: dropdown populated from the allowed-state list - **Initial context**: optional JSON object - **Allowed transitions**: optional editable list of legal `from -> to` transition rules - **Interval tab** (optional): config-owned periodic active-lifecycle scheduling - `Enable interval emissions` - `Interval ms` - `In-flight policy`: `skip` or `queue_one` - `Timing mode`: `fixed_rate` or `fixed_delay` ### Timing mode - **fixed_rate** Emit on a fixed wall-clock schedule (every `N` ms). The scheduler runs continuously and is not re-phased on state entry. Emissions do not wait for the previous active cycle to complete; if a prior cycle is still unresolved, overlap is handled by the configured in-flight policy. - **fixed_delay** Emit `N` ms after the previous active cycle completes. This guarantees a delay between completed cycles and avoids overlap by construction. In **fixed_rate** mode, the interval behaves like a controller-owned scan clock. It runs continuously and is not synchronized to the moment a state becomes active. State handlers execute in response to that clock only when the previous activation cycle has completed. Because `fixed_rate` is not synchronized to state entry, the first interval emission after entering a state may occur sooner than the full configured interval. ### Runtime behavior On startup the FSM initializes to: - `currentState = initialState` - `previousState = null` - `context = clone(initialContext || {})` - `eventId = 0` The config node accepts normalized transition requests from `dfsm-activate`. Allowed transitions are optional: - if no transition rules are configured, all valid state-to-state requests are allowed - if transition rules are configured, only listed transitions are legal - use `*` as the `from` state to allow a transition from any current state, for example `* -> FAULT` - use `*` as the `to` state to allow a transition to any valid target state, for example `STARTING -> *` Transition legality is enforced centrally in the FSM config runtime before any state, context, or event counter is mutated. `dfsm-state-machine` also owns active-lifecycle interval scheduling state. This internal state tracks: - current active state - whether one `dfsm-active` emission is currently unresolved/in flight - whether one pending interval emission is queued Same-state requests are handled by `dfsm-activate` in one of two mutually exclusive ways: - same-state completion in place (`retrigger` disabled): marks the current activation cycle complete while remaining in the same state - immediate same-state retrigger (`retrigger` enabled): emits an explicit retrigger event in the transition domain Same-state retriggers are not treated as state transitions for lifecycle purposes: - they do not dispatch `dfsm-state-exit` - they do not dispatch `dfsm-state-enter` - they do not resolve/clear the current active-cycle state used by interval scheduling Same-state completion (in-place) does resolve/clear the current active-cycle state and does not immediately redispatch `dfsm-active`. Only accepted state-changing requests resolve the current active cycle. When a request is accepted, it computes and publishes a normalized event: ```json { "state": "RUNNING", "prevState": "IDLE", "changed": true, "retrigger": false, "context": { "setpoint": 1.1 }, "eventId": 1, "timestamp": 1713260000000 } ``` When a request is rejected, the FSM state and retained context remain unchanged and a structured error event is published for `dfsm-error` subscribers. Example global transition table: ```json [ { "from": "IDLE", "to": "STARTING" }, { "from": "STARTING", "to": "RUNNING" }, { "from": "STARTING", "to": "*" }, { "from": "RUNNING", "to": "STOPPING" }, { "from": "STOPPING", "to": "IDLE" }, { "from": "*", "to": "FAULT" } ] ``` ## dfsm-activate Requests that the FSM transition to a target state and applies that request through the configured FSM instance. Conceptually, `dfsm-activate` is the transition-request node in the flow. ### Activation completion contract When `dfsm-active` emits a message, that represents one active-cycle dispatch from the FSM. Your handler flow must eventually signal what happened next by doing one of the following: - send a message to `dfsm-activate` with a different `nextState` to request a real transition - send a message to `dfsm-activate` with the same `nextState` to either: - complete in place, if **Retrigger on same state** is disabled - immediately retrigger, if **Retrigger on same state** is enabled - send a message to `dfsm-update-context` if you only need to mutate retained context and do not want to request a transition Important: simply finishing the downstream flow or returning from a function node does **not** by itself tell the FSM that the active cycle is complete. If interval scheduling is enabled, the FSM tracks whether an active-cycle dispatch is still in flight. A handler that never signals completion may prevent later interval-driven active emissions from occurring as expected. #### Example: periodic RUNNING work A `dfsm-active` handler for `RUNNING` checks a counter and either keeps running or stops: ```javascript if (msg.dfsm.context.count >= 5) { msg.dfsm = {nextState: "STOPPING"}; return msg; } msg.dfsm = { nextState: "RUNNING", context: { count: msg.dfsm.context.count + 1 } }; return msg; ``` ### Configuration - **FSM**: reference to a `dfsm-state-machine` node - **Retrigger on same state**: enabled by default; permits immediate same-state reactivation - **Default state**: optional fallback next state when no requested next state is provided ### Input contract Canonical transition requests are read from `msg.dfsm`: ```json { "dfsm": { "nextState": "RUNNING", "context": { "setpoint": 1.2 }, "replaceContext": false } } ``` ### Input semantics - canonical transition request field is `msg.dfsm.nextState` - legacy aliases `msg.nextState` and `msg.payload.nextState` are still accepted during the migration period - if none of `msg.dfsm.nextState`, legacy `msg.nextState`, legacy `msg.payload.nextState`, or configured `defaultState` is available, the request is rejected - the older local `dfsm-activate` present-state filter is currently disabled and ignored - the FSM config node applies its optional global allowed-transition rules - if the FSM config node rejects the requested `current state -> target state` pair as illegal, `dfsm-activate` warns and shows red `illegal transition` status - `msg.dfsm.context` shallow-merges into the retained FSM context - if `msg.dfsm.replaceContext` is `true`, `msg.dfsm.context` replaces the full retained FSM context - if the requested state matches the current state: - with **Retrigger on same state = true**, it immediately retriggers the same state - with **Retrigger on same state = false**, it marks the current activation complete in place (no transition, no immediate redispatch) Interval scheduling does not change the meaning of same-state requests. Interval timers are only a later trigger source. The FSM config node's allowed-transition table is the global machine rule. `dfsm-activate` currently applies transition checks in this order: 1. FSM config global allowed-transition check 2. transition application and event dispatch State-variable meanings are: - `prevState` = previous state - `state` = current state - `nextState` = requested next state For example, `dfsm-active` may emit: ```json { "dfsm": { "prevState": "STARTING", "state": "RUNNING" } } ``` A later request into `dfsm-activate` should use: ```json { "dfsm": { "nextState": "STOPPING" } } ``` `dfsm-activate` does not treat a prior snapshot `msg.dfsm.state` value as a transition request, and it also ignores legacy snapshot-like `msg.payload.state` values when `nextState` is absent. When no custom name is set, `dfsm-activate` displays its configured `defaultState` as its node label. ### Output behavior `dfsm-activate` does not emit a normal output message itself. - accepted requests cause `dfsm-state-machine` to publish events consumed by `dfsm-active` - rejected requests cause `dfsm-state-machine` to publish structured errors consumed by `dfsm-error` ## dfsm-update-context Updates the retained FSM context without requesting a state transition. Use this when a handler needs to mutate shared machine data (counters, flags, timestamps, measurements) but should not change state. ### Configuration - **FSM**: reference to a `dfsm-state-machine` node - **Mode**: - `merge` (default): shallow-merge patch into retained context - `replace`: replace retained context object ### Input contract Canonical context updates are read from `msg.dfsm`: ```json { "dfsm": { "context": { "metrics": { "ticks": 4 } }, "state": "RUNNING" } } ``` - `msg.dfsm.context` is required and must be a plain object - `msg.dfsm.state` is optional - if provided, it must match the current active FSM state - if omitted, the current active FSM state is used - legacy compatibility is retained for `msg.payload.context` / `msg.payload.state` and top-level `msg.context` / `msg.state` ### Runtime semantics - applies context update through FSM-owned merge/replace logic (same semantics as transition context updates) - does not call transition APIs - does not change `state`/`prevState` - does not increment `eventId` - does not emit `dfsm-active`, `dfsm-state-enter`, or `dfsm-state-exit` lifecycle events - does not affect interval scheduling state ### Output behavior Pass-through: forwards the incoming message unchanged. ## dfsm-active Subscribes to active-lifecycle emissions from `dfsm-state-machine` and emits them into the flow for explicit state-handler logic. Conceptually, `dfsm-active` emits the handler flow for a state while that state is active. Users familiar with IEC SFC may see some similarity to an `N`-style active action, but this node operates within Node-RED's event-driven runtime. ### Configuration - **FSM**: reference to a `dfsm-state-machine` node - **Emit all FSM events**: when enabled, emit every accepted event - **Resulting state**: when "all" is disabled, only emit events whose resulting state matches this value ### Input This node does not receive flow input messages. ### Output contract Writes the FSM snapshot to `msg.dfsm` and preserves `msg.payload` for application/work data: ```json { "dfsm": { "state": "RUNNING", "prevState": "IDLE", "changed": true, "retrigger": false, "context": { "setpoint": 1.1 }, "eventId": 3, "timestamp": 1713260000000 } } ``` Use this node to trigger the handler flow for one state, or for all states. When interval scheduling is enabled in `dfsm-state-machine`, periodic emissions are lifecycle signals (for example `msg.dfsm.lifecycleType = "active_interval"`), not transition retriggers. `dfsm-active` publishes state snapshots, not transition requests. Transition-request fields such as `nextState` are scrubbed from outgoing messages. ## dfsm-error Subscribes to explicit FSM errors so rejection paths remain visible in the flow. ### Configuration - **FSM**: reference to a `dfsm-state-machine` node ### Input This node does not receive flow input messages. ### Output contract Writes a structured FSM error to `msg.dfsm.error` and preserves `msg.payload` for application/work data: ```json { "dfsm": { "error": { "type": "invalid_state", "message": "Requested state \"SANDWICH\" is not allowed.", "requestedState": "SANDWICH", "currentState": "RUNNING", "validStates": [ "RUNNING", "STOPPING", "STOPPED" ], "originalRequest": { "state": "SANDWICH" }, "ts": 1713260000000 } } } ``` Typical first-pass error types include: - `invalid_state` - `missing_state` - `malformed_payload` - `non_object_context` - `missing_context` - `illegal_transition` Global illegal transitions are rejected before state mutation, produce red `illegal transition` status on `dfsm-activate`, and can be observed through `dfsm-error`. ## dfsm-summary Generates a summary of a selected `dfsm-state-machine` when it receives an input message. Output can be plain Markdown ( default) or clean HTML suitable for dashboard template nodes. ### Configuration - **FSM**: reference to a `dfsm-state-machine` node - **Format**: `markdown` (default) or `html` ### Input One message input. Any received message triggers summary generation. ### Output contract Replaces `msg.payload` with a string containing: - state machine name - initial state - state list - allowed transition list - interval settings summary (enabled, interval ms, and configured policy/mode) **Markdown mode** emits plain Markdown text using headings (`#`, `##`) and bullet lists (`-`). **HTML mode** emits clean HTML using standard tags (`