The package is an optional interface plugin, but its process is an independent
companion started with fastsim-tui. It isn't a managed Runtime plugin and doesn't
publish fastsim.components or fastsim.plugins entry points. It imports no FastSim
Core or simulator SDK. When the TUI isn't running, FastSim creates no TUI object,
callback, thread, or network task.
fastsim-tui fastsim-server FastSim Run
┌──────────────────────┐ HTTP/WS ┌──────────────────────┐ API ┌──────────────┐
│ Textual workstation │◄─────────►│ public control plane │◄───────►│ UniRoboSim │
│ independent process │ v1 │ generation authority │ │ backend │
└──────────────────────┘ └──────────────────────┘ └──────────────┘
Install and connect
Install the TUI in any Python 3.11 or 3.12 environment that can reach the Server. It doesn't need to be installed in the simulator environment.
python -m pip install ./packages/fastsim-plugin-tui
fastsim-tui --version
fastsim-tui --url http://127.0.0.1:8000 --connect
For an authenticated Server, prefer an environment variable or a protected token file:
FASTSIM_SERVER_TOKEN='...' fastsim-tui --url https://sim.example.com --connect
fastsim-tui --url https://sim.example.com --token-file /run/secrets/fastsim-token --connect
The default transport is the authenticated incremental WebSocket protocol. A remote deployment may require the exact Origin configured by the Server:
fastsim-tui \
--url https://sim-api.example.com \
--realtime-origin https://console.example.com \
--connect
Polling is a visible compatibility mode, never a silent fallback:
fastsim-tui --url http://127.0.0.1:8000 --transport compatibility-poll --connect
Workstation surfaces
| View | Behavior |
|---|---|
| Dashboard | Run generation, lifecycle, tick, simulation time, reported FPS, measured tick rate, and current subscriptions |
| Launch | Server-root or bounded local YAML/JSON/TOML launch, with an open, prepared, or running target state |
| Scene | Generation-cached, paged entity catalog and latest-wins entity state |
| Camera | Demand-only Server JPEG/PNG snapshot preview; leaving the tab stops requests |
| Control | Discovered control targets, joint paths, physical tracks, preemption, timeout, and applied-control audit |
| Plugins | Plugin instance status and reliable plugin events |
| Operations | Progress, exact operation detail, accepted operation identity, and cancellation |
| Logs | Reliable Server log stream plus client transport/protocol failures |
| Metrics | Demand-only process CPU, resident memory, and optional GPU-memory samples |
The workstation uses a restrained high-contrast palette and reflows at compact, standard, and wide terminal breakpoints. Overview keeps lifecycle, simulation time, tick, FPS, measured tick rate, generation, Run identity, and active realtime demand visible without requiring the operator to read raw JSON. A persistent activity line keeps the latest success or exact error visible after notifications disappear.
Press 1 through 9 to move directly between views, or Ctrl+P to search the command
palette. Keyboard lifecycle actions pass through the same Server admission gates as
their buttons; a shortcut cannot submit an operation that the current Run does not
allow.
On terminals narrower than 124 columns, the tab strip becomes one compact view button. The button always names the active view and opens a scrollable list containing all nine views, so a clipped tab can never make a surface undiscoverable. Help and confirmation dialogs also size to the current terminal and scroll their content when needed.
Connection colors are semantic: green means an established realtime or explicit poll
session, amber means connecting, reconnecting, or degraded, and red means failed. An
initial connection failure returns the Connect control to an actionable failed state
rather than leaving the workstation at CONNECTING.
Lifecycle buttons are derived from both Server capabilities and the Run's
allowed_lifecycle_actions. For example, Step is disabled while a Run is running and
enabled only when the Server admits it. Step count is explicit and bounded. Every
Run-scoped mutation carries the current slot generation.
All mutations pass through one serialized client lane. Ambiguous mutation responses are not retried blindly. The returned operation ID remains visible so the operator can inspect progress or request cancellation.
Joint path
Select a control target or enter the actor and resource group, set dt, preemption and
an optional timeout, then provide a rectangular JSON path:
[
[0.0, -0.4, 0.2],
[0.1, -0.3, 0.3]
]
Physical tracks
Physical tracks expose the complete Server contract for multi-resource commands. Each track declares a versioned controller and command space, stable axes and units, and an explicit timeline:
{
"tracks": [
{
"entity_id": "robots.droid",
"resource_group": "arm",
"controller": "joint.passthrough@1",
"command_space": "joint.position@1",
"interpolation": "linear",
"frames": [
{
"time_from_start_s": 0.0,
"axes": ["joint1", "joint2"],
"values": [0.0, 0.0],
"units": ["rad", "rad"]
},
{
"time_from_start_s": 1.0,
"axes": ["joint1", "joint2"],
"values": [0.2, -0.2],
"units": ["rad", "rad"]
}
]
}
]
}
The TUI validates finite values, identifier syntax, versioned IDs, stable axes/units, non-overlapping resources, frame and byte bounds before submission.
Transport and performance
- REST is limited to discovery, ticket creation, mutations, camera snapshots, catalog pages and details explicitly requested by the operator.
- The active tab selects one bounded set of realtime subscriptions. Hidden tabs create no state, scene, plugin, log, metric, operation or applied-control sampling demand.
- Realtime state uses snapshot plus JSON Patch delta, heartbeat acknowledgement, generation-aware resubscription, cursored resume and bounded exponential reconnect.
- Scene catalog data is cached by slot generation. State is latest-wins; events, logs and applied control use fixed-capacity histories.
- JSON responses are byte-bounded before parsing and decoded once. Non-standard non-finite JSON constants are rejected during parsing; finite/type checks are applied to the metrics, camera time, simulation time, FPS, and control values the TUI actually consumes instead of recursively rescanning every response value.
- Camera preview requests encoded JPEG by default (
--camera-format pngis explicit), owns one worker and one current frame, and cannot accumulate a frame backlog. - Disconnect and application teardown stop timers, subscriptions, camera work, the HTTP pool and WebSocket tasks.
Keyboard reference
| Key | Action |
|---|---|
1–9 |
Open Overview through Metrics |
Ctrl+P |
Search navigation and workstation commands |
C |
Connect or disconnect |
Space |
Pause or resume when admitted |
N |
Step the configured number of ticks when admitted |
R |
Reset with confirmation |
S |
Stop with confirmation |
F5 |
Explicit refresh in compatibility mode |
? |
Help |
Q |
Quit and cleanly close client resources |
Development verification
python -m pytest -q
python -m ruff check src tests
python -m ruff format --check src tests
python -m mypy src
python -m build
The test extra includes the official pytest-textual-snapshot plugin. Accepted SVG
snapshots cover 80x24, 88x32, and 160x50 terminals plus compact navigation, help,
connected/error states, and long confirmation content.
Run pytest --snapshot-update only after inspecting the generated visual diff; it is a
human approval action, not a formatting command.
Package tests cover bounded HTTP behavior, structured failures, public generation and
idempotency headers, realtime demand selection, heartbeat, generation changes, deltas,
reconnect/resume, Textual interaction and clean teardown. The package release gate also
runs against the HTTP/WebSocket stack of Server 0.7.6 as a tested version, an installed Wheel, repeated
Textual teardown, and a bounded resource soak. A visible Isaac DROID Run remains a
separate backend acceptance gate rather than a claim made by the simulator-free package.