-
Couldn't load subscription status.
- Fork 4
Description
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"]
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 controlBenefits:
- 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
eventBusand 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 fileCommand 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)
- Create
@bluecadet/launchpad-controllerpackage - completed in Introduce Launchpad Controller #249 - Implement EventBus, StateStore, CommandDispatcher - completed in Introduce Launchpad Controller #249
- Update content/monitor to emit events - completed in Introduce Launchpad Controller #249
- All CLI commands use controller in task mode - completed in Introduce Launchpad Controller #249
- No config changes required
- No breaking changes (behavior identical)
Phase 2: Persistent Mode + IPC
- Add controller mode enum (
task|persistent) - completed in Add Persistent/Daemonized Controller #257 - Implement IPC transport - completed in Add Persistent/Daemonized Controller #257
- Add
launchpad startcommand (persistent mode) - completed in Add Persistent/Daemonized Controller #257 - Add
launchpad status,launchpad stopcommands - completed in Add Persistent/Daemonized Controller #257 - Task commands auto-detect persistent controller - completed in Add Persistent/Daemonized Controller #257
Phase 3: WebSocket + Dashboard
- Implement WebSocket transport
- Build dashboard (see DASHBOARD_PROPOSAL.md)
- Dashboard connects to WebSocket transport
Phase 4: Extended Transports
- OSC transport package?
- MQTT transport package?
- Document custom transport API for third-party devs
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 controllerOpen 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-controllerpackage - Begin EventBus and StateStore implementation