AKA 'mission control'?
Overview
This proposal introduces a controller layer for Launchpad that enables multi-interface control and real-time event observation. The controller acts as a centralized event bus and command dispatcher, allowing multiple interfaces to observe and control the same Launchpad instance simultaneously.
Key Additions: Controller operates in two modes:
Task Mode - Ephemeral, runs a single command and exits (like current behavior)
Persistent Mode - Long-running, enables transports for multi-interface control
Control Interfaces Enabled (Persistent Mode Only):
CLI commands (via IPC transport)
Web dashboard (via WebSocket transport)
OSC (Open Sound Control)
addtl protocols (MQTT, HTTP webhooks, etc.)
Problem
We want to add a dashboard and other ways of controlling the Launchpad process, but Launchpad currently follows a lifecycle-based, one-shot execution model :
CLI Command
↓
Load Config & Environment
↓
Initialize Logger
↓
Instantiate Subsystem(s)
↓
Call Imperative Methods (content.start(), monitor.start())
↓
Return (process exit or keep running)
Current Limitations:
Each command is independent and instantiates subsystems directly
No persistent inter-subsystem communication
No way to dynamically control a running instance
No observability beyond logs
Cannot have multiple control interfaces (CLI + web UI + OSC)
Example: You cannot start Launchpad with launchpad start, then in another terminal run launchpad app restart myapp to restart a single app.
Proposed Architecture
High-Level Overview
flowchart TD
subgraph TASK["Task Mode (Ephemeral)"]
CLI1["launchpad content"]
CLI2["launchpad app restart foo"]
end
subgraph PERSIST["Persistent Mode (Long-Running)"]
START["launchpad start / start -d"]
Controller["Controller with Transports"]
IPC["IPC Transport"]
WS["WebSocket Transport"]
OSC["OSC Transport"]
end
CLI1-->|Inline execution|Content
CLI2-->|Inline execution|Monitor
START-->Controller
Controller-->IPC
Controller-->WS
Controller-->OSC
IPC-->CLIClient["CLI Commands"]
WS-->Dashboard["Web Dashboard"]
OSC-->External["TouchDesigner/Max"]
Content["Content Subsystem"]
Monitor["Monitor Subsystem"]
Loading
Controller Modes
Task Mode:
Starts controller inline
NO transports (no IPC, WebSocket, OSC)
Executes single command
Exits immediately when done
Perfect for one-shot operations
Persistent Mode:
Starts controller with all configured transports
Enables multi-interface control
Stays alive until stopped or Ctrl+C
Can run foreground (blocks) or daemon (background)
Perfect for interactive installations, remote control, dashboards
Architecture Principles
1. Singleton Controller Instance
There is ONE controller instance with ONE EventBus and ONE StateStore, shared by all transports.
Rationale:
Single source of truth - All clients see the same state
Consistent events - One event emitted → all transports notified
No synchronization - No need to sync state across instances
Simpler reasoning - Easier to debug, test, and understand
2. Always Use Controller (Unified Code Path)
ALL commands go through the controller, in either task or persistent mode.
// ✅ Task Mode (ephemeral)
launchpad content
// → Controller starts in task mode (no transports)
// → controller.executeCommand({ type: 'content.fetch' })
// → Events emitted (to EventBus, but no transports to forward them)
// → Exits when done
// ✅ Persistent Mode (long-running)
launchpad start
// → Controller starts in persistent mode (with transports)
// → Transports forward events to clients
// → Stays alive for dynamic control
Benefits:
Single code path - Content/monitor always emit events
Consistent state - StateStore always updated
No branching - One testing path, simpler logic
Future-proof - Easy to add features (all commands use controller)
3. Transport Lifecycle Tied to Mode
Transports only start in persistent mode.
Rationale:
No surprise processes - Task commands don't leave controllers running
Resource efficient - No unused sockets/ports in task mode
Clear semantics - start = persistent, everything else = task
Logical - Transports need persistent controller to be useful
Core Components
1. EventBus
Type-safe event system for inter-subsystem communication
Usage:
Task mode - Events emitted but not forwarded (no transports)
Persistent mode - Events emitted and forwarded to all transports
Plugins - Can subscribe and emit in both modes
2. StateStore
Maintains real-time state by listening to events
Characteristics:
Updated in both task and persistent modes
Only queryable via transports in persistent mode
Ephemeral in task mode (lost when controller exits)
3. CommandDispatcher
Executes commands on subsystems and emits corresponding events
Command Flow:
Client/CLI → CommandDispatcher → Subsystem
↓
EventBus
↓
All Transports (if persistent)
↓
All Clients
4. Transport System
Transports follow the same factory function pattern as content sources:
Built-in Transports:
IPC - Unix socket / named pipe for CLI communication
WebSocket - Real-time web communication, e.g. dashboard
...
Transport responsibilities:
Listen for incoming commands and dispatch via commandDispatcher
Subscribe to events via eventBus and forward to clients
Handle client connections and subscriptions
Only active in persistent mode
CLI Integration
Command Modes
Command
Mode
Transports
Behavior
launchpad start
Persistent (foreground)
✅ All
Blocks until Ctrl+C
launchpad start -d
Persistent (daemon)
✅ All
Background, returns immediately
launchpad content
Task*
❌ None
Executes and exits
launchpad app restart <name>
Task*
❌ None
Executes and exits
launchpad status
N/A
N/A
Queries persistent controller via IPC
launchpad logs -f
N/A
N/A
Streams from persistent controller via IPC
launchpad stop
N/A
N/A
Stops persistent controller
* If persistent controller running, sends via IPC instead
Command Examples
# 1. Start persistent controller (foreground)
launchpad start
# → Controller starts with transports (IPC, WebSocket, OSC)
# → Fetches content
# → Starts apps
# → Logs to stdout
# → Blocks until Ctrl+C
# 2. Start persistent controller (daemon)
launchpad start -d
# → Forks controller process
# → Returns immediately
# → Logs to ~/.launchpad/logs/launchpad.log
# → PID written to ~/.launchpad/launchpad.pid
# 3. Task: Fetch content (no persistent controller)
launchpad content
# → Controller starts in task mode (no transports)
# → Runs content.fetch command
# → Exits when complete
# 4. Task: Fetch content (persistent controller running)
launchpad content
# → Detects persistent controller (IPC socket exists)
# → Sends command via IPC
# → Returns immediately
# 5. Control apps via persistent controller
launchpad app start video-player
# → If persistent controller running: sends via IPC
# → If no persistent controller: runs in task mode
# 6. Query persistent controller
launchpad status
# → Requires persistent controller
# → Queries state via IPC
# → Pretty-prints status
# 7. Stream logs from persistent controller
launchpad logs -f
# → Requires persistent controller
# → Subscribes to log events via IPC
# → Streams to stdout
# 8. Stop persistent controller
launchpad stop
# → Sends shutdown command via IPC
# → Fallback: SIGTERM via PID file
Command Behavior Matrix
Command
No Persistent Controller
Persistent Controller Running
launchpad start
Start persistent (foreground)
Error: Already running
launchpad start -d
Start persistent (daemon)
Error: Already running
launchpad content
Task mode (inline, exits)
Send via IPC (instant)
launchpad app restart foo
Task mode (inline, exits)
Send via IPC (instant)
launchpad status
Error: No persistent controller
Query via IPC
launchpad logs -f
Error: No persistent controller
Stream via IPC
launchpad stop
N/A
Stop persistent controller
Configuration
import { defineConfig } from '@bluecadet/launchpad-cli' ;
import ipcTransport from '@bluecadet/launchpad-controller/transports/ipc' ;
import websocketTransport from '@bluecadet/launchpad-controller/transports/websocket' ;
import oscTransport from '@bluecadet/launchpad-osc-transport' ;
export default defineConfig ( {
controller : {
transports : [
ipcTransport ( {
socketPath : '/tmp/launchpad.sock'
} ) ,
websocketTransport ( {
port : 3001 ,
cors : { enabled : true , origins : [ 'http://localhost:5173' ] }
} ) ,
oscTransport ( {
localPort : 7400 ,
remotePort : 7500 ,
mappings : {
'/art/video/play' : { type : 'app.start' , appName : 'video-player' } ,
'/art/video/stop' : { type : 'app.stop' , appName : 'video-player' } ,
'/art/content/refresh' : { type : 'content.fetch' }
}
} )
]
} ,
content : {
sources : [ /* ... */ ] ,
plugins : [ /* ... */ ]
} ,
monitor : {
apps : [ /* ... */ ] ,
plugins : [ /* ... */ ]
}
} ) ;
Plugin Hook Alignment
Plugin hook names will be updated to directly mirror EventBus event names.
// Plugin hooks = EventBus events
export default function myPlugin ( ) : Plugin {
return {
name : 'my-plugin' ,
hooks : {
// instead of onAppStarted
'monitor:app:started' : async ( ctx , { appName, pid } ) => {
ctx . logger . info ( `App ${ appName } started` ) ;
} ,
// instead of onContentFetchComplete
'content:fetch:complete' : async ( ctx , { success } ) => {
ctx . logger . info ( 'Content fetch complete' ) ;
} ,
// Custom events
'installation:motion:detected' : async ( ctx , { zone } ) => {
ctx . logger . info ( `Motion in zone ${ zone } ` ) ;
}
}
} ;
}
Rationale:
Predictable - Event name = hook name
Discoverable - EventBus types = available hooks
Flexible - Easy to add custom events
Migration Path
Phase 1: Core Controller (Task Mode)
Phase 2: Persistent Mode + IPC
Phase 3: WebSocket + Dashboard
Phase 4: Extended Transports
Backward Compatibility:
# New behavior (via controller task mode)
launchpad content # Fetch content, exit (via controller)
# New capability (persistent mode)
launchpad start -d # Run persistent controller
launchpad content # Send to persistent controller
Open Questions
State persistence - Should StateStore persist to disk for daemon restarts?
Command queuing - Should commands be queued if subsystems are busy?
Plugin coordination - Should plugins be able to coordinate across modes?
Daemon lifecycle - Should daemon auto-restart on crashes?
Package Deps - How do we keep monitor/content as optional dependencies?
Conclusion
This proposal introduces a controller layer with two distinct modes:
Task Mode - Ephemeral execution, no transports, exits immediately (default for all commands except start)
Persistent Mode - Long-running with transports, enables multi-interface control (only launchpad start)
This architecture:
Maintains backward compatibility (task mode = current behavior)
Enables advanced features (persistent mode = multi-interface control)
Avoids surprise processes (task commands always exit)
Follows Docker-style patterns (foreground vs. daemon)
Provides clear upgrade path (opt-in to persistent mode)
Next Steps:
Review and gather feedback on this proposal
Create implementation tasks for Phase 1
Set up @bluecadet/launchpad-controller package
Begin EventBus and StateStore implementation
AKA 'mission control'?
Overview
This proposal introduces a controller layer for Launchpad that enables multi-interface control and real-time event observation. The controller acts as a centralized event bus and command dispatcher, allowing multiple interfaces to observe and control the same Launchpad instance simultaneously.
Key Additions: Controller operates in two modes:
Control Interfaces Enabled (Persistent Mode Only):
Problem
We want to add a dashboard and other ways of controlling the Launchpad process, but Launchpad currently follows a lifecycle-based, one-shot execution model:
Current Limitations:
Example: You cannot start Launchpad with
launchpad start, then in another terminal runlaunchpad app restart myappto restart a single app.Proposed Architecture
High-Level Overview
flowchart TD subgraph TASK["Task Mode (Ephemeral)"] CLI1["launchpad content"] CLI2["launchpad app restart foo"] end subgraph PERSIST["Persistent Mode (Long-Running)"] START["launchpad start / start -d"] Controller["Controller with Transports"] IPC["IPC Transport"] WS["WebSocket Transport"] OSC["OSC Transport"] end CLI1-->|Inline execution|Content CLI2-->|Inline execution|Monitor START-->Controller Controller-->IPC Controller-->WS Controller-->OSC IPC-->CLIClient["CLI Commands"] WS-->Dashboard["Web Dashboard"] OSC-->External["TouchDesigner/Max"] Content["Content Subsystem"] Monitor["Monitor Subsystem"]Controller Modes
Task Mode:
Persistent Mode:
Architecture Principles
1. Singleton Controller Instance
There is ONE controller instance with ONE EventBus and ONE StateStore, shared by all transports.
Rationale:
2. Always Use Controller (Unified Code Path)
ALL commands go through the controller, in either task or persistent mode.
Benefits:
3. Transport Lifecycle Tied to Mode
Transports only start in persistent mode.
Rationale:
start= persistent, everything else = taskCore Components
1. EventBus
Type-safe event system for inter-subsystem communication
Usage:
2. StateStore
Maintains real-time state by listening to events
Characteristics:
3. CommandDispatcher
Executes commands on subsystems and emits corresponding events
Command Flow:
4. Transport System
Transports follow the same factory function pattern as content sources:
Built-in Transports:
Transport responsibilities:
commandDispatchereventBusand forward to clientsCLI Integration
Command Modes
launchpad startlaunchpad start -dlaunchpad contentlaunchpad app restart <name>launchpad statuslaunchpad logs -flaunchpad stop* If persistent controller running, sends via IPC instead
Command Examples
Command Behavior Matrix
launchpad startlaunchpad start -dlaunchpad contentlaunchpad app restart foolaunchpad statuslaunchpad logs -flaunchpad stopConfiguration
Plugin Hook Alignment
Plugin hook names will be updated to directly mirror EventBus event names.
Rationale:
Migration Path
Phase 1: Core Controller (Task Mode)
@bluecadet/launchpad-controllerpackage - completed in Introduce Launchpad Controller #249Phase 2: Persistent Mode + IPC
task|persistent) - completed in Add Persistent/Daemonized Controller #257launchpad startcommand (persistent mode) - completed in Add Persistent/Daemonized Controller #257launchpad status,launchpad stopcommands - completed in Add Persistent/Daemonized Controller #257Phase 3: WebSocket + Dashboard
Phase 4: Extended Transports
Backward Compatibility:
Open Questions
Conclusion
This proposal introduces a controller layer with two distinct modes:
start)launchpad start)This architecture:
Next Steps:
@bluecadet/launchpad-controllerpackage