Skip to content

Multi-Interface Control #215

@claytercek

Description

@claytercek

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

  • 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 controller

Open Questions

  1. State persistence - Should StateStore persist to disk for daemon restarts?
  2. Command queuing - Should commands be queued if subsystems are busy?
  3. Plugin coordination - Should plugins be able to coordinate across modes?
  4. Daemon lifecycle - Should daemon auto-restart on crashes?
  5. 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:

  1. Review and gather feedback on this proposal
  2. Create implementation tasks for Phase 1
  3. Set up @bluecadet/launchpad-controller package
  4. Begin EventBus and StateStore implementation

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions