Skip to content

How It Works

Josh Bowden edited this page Mar 25, 2026 · 4 revisions

A technical overview of FXCommands for contributors and curious users.


Architecture

Stream Deck (hardware/software)
    |
    |  KeyDown / KeyUp events via Stream Deck SDK
    v
FXCommandAction (src/actions/fx-command.ts)
    |
    |  Parses delay syntax, manages button state
    v
ConnectionManager (src/connection-manager.ts)
    |
    |  TCP socket to localhost:29200
    v
FiveM / RedM Console

The plugin runs as a Node.js process managed by the Stream Deck software. It communicates with the Stream Deck via the Elgato Stream Deck SDK 2.0 and with the game via a raw TCP connection.


CMND Protocol

FiveM and RedM expose a local console server on port 29200 that accepts commands via a binary protocol called CMND (IceCon-style).

Frame layout

Each command is wrapped in a binary frame:

Offset  Size    Field         Value
0       4       Magic         0x43 0x4D 0x4E 0x44 ("CMND")
4       2       Protocol      0x00 0xD3 (211, big-endian)
6       4       Length        Command byte length + 1 (big-endian uint32)
10      2       Padding       0x00 0x00
12      N       Command       UTF-8 encoded string + newline (\n)
12+N    1       Terminator    0x00

The ConnectionManager builds this frame and writes it to the TCP socket. The game processes the command as if it were typed into the console.


Connection Lifecycle

  1. Pre-connect on appear - When a button appears on the Stream Deck (profile load, app start), the plugin opens a TCP connection to localhost:29200 so the first key press sends immediately with no connection delay.

  2. Lazy reconnect - If the connection drops (game closed, network reset), the next send() call attempts to reconnect before sending.

  3. Persistent socket - A single TCP connection is reused across all button presses. The socket uses TCP_NODELAY for minimal latency and keepAlive (10s interval) to detect dead connections.

  4. Graceful cleanup - When a button disappears (profile switch, plugin unload), its state is cleaned up from memory.


Command Processing

Simple commands

A plain string like e wave is sent directly as a single CMND frame.

Chained commands (;)

A string like e sit;me relaxes is sent as-is in a single frame. FiveM natively supports ; as a command separator, so the game handles the splitting.

Delayed commands (;; / {NNNms})

The plugin splits the input into tokens:

Input:  "e think;me thinking;{2000ms};e c"

Tokens: [
  { type: "cmd",   value: "e think;me thinking" },
  { type: "delay", ms: 2000 },
  { type: "cmd",   value: "e c" }
]

Each command token is sent as a separate CMND frame, with await sleep(ms) between delay tokens. Single ; within a command token is preserved (passed to FiveM for native chaining).


State Machine

Each button instance tracks its own current stage (0-4). The state machine works as follows:

  1. Key down - Read the current stage, execute that stage's On Press command
  2. Key up - Execute the current stage's On Release command, then advance to the next stage
  3. Stage wrap - nextState = (currentState + 1) % desiredStates
  4. Visual update - The button icon updates to reflect the new stage (Stream Deck supports up to 5 visual states)

State is stored both in-memory (for fast access) and in the Stream Deck settings (for persistence across restarts).


Property Inspector

The settings UI (PluginActionPI.html) communicates with the plugin via the Stream Deck WebSocket bridge:

  • Plugin → PI: Settings are sent when the PI opens
  • PI → Plugin: Settings are sent whenever the user changes a value
  • The PI dynamically shows/hides command fields based on the Number of States setting

See Also

Clone this wiki locally