Context
MeticAI currently has two deployment targets that speak to the machine via different paths:
- Docker/Web — React app → MeticAI Python server → MQTT (meticulous-addon) → machine
- 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
Context
MeticAI currently has two deployment targets that speak to the machine via different paths:
To support both from the same codebase without a fork, a
MachineServiceabstraction 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
MachineServiceTypeScript interface with aMeticAIAdapterimplementation 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
Interface Design
Files to Create
apps/web/src/services/machine/MachineService.tsapps/web/src/services/machine/MeticAIAdapter.tsapps/web/src/services/machine/MachineServiceContext.tsxapps/web/src/hooks/useMachineService.tsFiles to Update
apps/web/src/lib/mqttCommands.tsMeticAIAdapterdelegates to it — no deletionapps/web/src/hooks/useWebSocket.tsmqttCommandsdirectlyuseMachineService()insteadapps/web/src/App.tsxMachineServiceProviderMeticAIAdapter Implementation Notes
The adapter is a thin delegation layer — it wraps what already exists:
mqttCommands.ts(unchanged)useWebSocket(unchanged)The adapter does not introduce any new network calls or behaviour.
What This Enables
Once in place, a future
MachineDirectAdaptercan 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
MachineDirectAdapterimplementation (separate issue)Acceptance Criteria
MachineServiceinterface defined and exportedMeticAIAdapterpasses all existing functionality through correctlyuseMachineService()hook available and used by at least the primary machine-control components (LiveShotView, ControlCenter, PourOverView)