Skip to content

Introduce MachineService abstraction layer for multi-backend support #252

@hessius

Description

@hessius

Context

MeticAI currently has two deployment targets that speak to the machine via different paths:

  1. Docker/Web — React app → MeticAI Python server → MQTT (meticulous-addon) → machine
  2. iOS (future) — React app → machine's native HTTP REST + Socket.IO directly

To support both from the same codebase without a fork, a MachineService abstraction needs to sit between UI components and machine communication. This is a non-breaking, behaviour-identical refactor — it reorganises existing code behind a typed interface without changing any visible functionality.

Goal

Introduce a MachineService TypeScript interface with a MeticAIAdapter implementation that wraps the current stack. All machine commands and telemetry subscriptions route through this interface. Future adapters can be added without touching any component code.

Architecture

Components / Hooks
      ↓
useMachineService()   ← React context hook
      ↓
MachineService        ← TypeScript interface
      ↓
MeticAIAdapter        ← Wraps existing mqttCommands.ts + useWebSocket
      ↓
MeticAI Python server (unchanged)
      ↓
meticulous-addon / pyMeticulous (untouched)

Interface Design

// apps/web/src/services/machine/MachineService.ts

export interface MachineService {
  // Commands
  start(): Promise<CommandResult>
  stop(): Promise<CommandResult>
  abort(): Promise<CommandResult>
  continue(): Promise<CommandResult>
  tare(): Promise<CommandResult>
  preheat(): Promise<CommandResult>
  purge(): Promise<CommandResult>
  homePlunger(): Promise<CommandResult>
  loadProfile(name: string): Promise<CommandResult>
  setBrightness(value: number): Promise<CommandResult>
  enableSounds(enabled: boolean): Promise<CommandResult>

  // Telemetry
  subscribe(listener: (state: MachineState) => void): () => void

  // Profiles & history (async data)
  listProfiles(): Promise<Profile[]>
  getProfile(id: string): Promise<Profile>
  createProfile(profile: Profile): Promise<Profile>
  deleteProfile(id: string): Promise<void>
  getLastProfile(): Promise<Profile | null>

  // Shot history
  getHistory(): Promise<ShotSummary[]>
  getShot(id: string): Promise<Shot>
}

export type CommandResult = { success: boolean; message?: string }

Files to Create

File Purpose
apps/web/src/services/machine/MachineService.ts Interface + shared types
apps/web/src/services/machine/MeticAIAdapter.ts Implementation wrapping current server API
apps/web/src/services/machine/MachineServiceContext.tsx React context provider
apps/web/src/hooks/useMachineService.ts Consumer hook

Files to Update

File Change
apps/web/src/lib/mqttCommands.ts Keep as-is; MeticAIAdapter delegates to it — no deletion
apps/web/src/hooks/useWebSocket.ts Keep as-is; adapter wraps it for subscribe()
Components using mqttCommands directly Import via useMachineService() instead
apps/web/src/App.tsx Wrap with MachineServiceProvider

MeticAIAdapter Implementation Notes

The adapter is a thin delegation layer — it wraps what already exists:

  • Commands → delegate to functions in mqttCommands.ts (unchanged)
  • subscribe() → delegate to useWebSocket (unchanged)
  • Profiles / history → delegate to existing API fetch calls in route-specific hooks

The adapter does not introduce any new network calls or behaviour.

What This Enables

Once in place, a future MachineDirectAdapter can be written against the Meticulous machine's native API (/api/v1/* + Socket.IO) and dropped in — zero component changes required. Adapter selection is handled at the provider level via build config or runtime environment detection.

Out of Scope

  • MachineDirectAdapter implementation (separate issue)
  • Any changes to the Python backend
  • Any changes to meticulous-addon or pyMeticulous

Acceptance Criteria

  • MachineService interface defined and exported
  • MeticAIAdapter passes all existing functionality through correctly
  • useMachineService() hook available and used by at least the primary machine-control components (LiveShotView, ControlCenter, PourOverView)
  • All existing tests continue to pass
  • No visible behaviour change for end users

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions