Skip to content

iOS app: native client via Capacitor + MachineDirectAdapter #253

@hessius

Description

@hessius

Overview

Build a native iOS app that lets users control their Meticulous espresso machine directly — with no Pi, no server, and no Docker stack required. Users download the app, connect to the same Wi-Fi as their machine, and are up and running.

This issue tracks everything from #252 onward. #252 (MachineService abstraction) is a prerequisite — the iOS work should not begin until that is in place and the web app is sufficiently mature.

Why This Is Possible

The Meticulous machine exposes a full native API on port 80 of the local network:

  • Socket.IO (/socket.io/) — real-time telemetry: sensors, weight, pressure, flow, state
  • REST (/api/v1/) — full profile CRUD, shot history, machine commands, settings

The current MeticAI Python server is a translation and orchestration layer on top of these. An iOS app on the same Wi-Fi can bypass it entirely.

Current:  Phone → MeticAI server (Pi) → MQTT bridge → Socket.IO → Machine
iOS app:  Phone → HTTP REST + Socket.IO → Machine (directly)

Architecture

┌──────────────────────────────────────┐
│  Existing React web app (unchanged)  │
│                                      │
│  useMachineService()                 │
│         ↓                            │
│  MachineService interface            │
│    ↓               ↓                 │
│  MeticAIAdapter   MachineDirectAdapter  ← new, in this issue
│  (Docker/web)     (iOS)              │
└──────────────────────────────────────┘
         ↓                    ↓
   MeticAI server       meticulous-{id}.local
   (Pi, unchanged)      HTTP REST + Socket.IO

The React codebase is shared. Only the adapter changes per deployment target.

Part 1: MachineDirectAdapter

Implement MachineDirectAdapter against the machine's native API.

Machine Discovery

The machine's mDNS name is randomised per device (e.g. meticulous-a3f7.local). Discovery options:

  1. mDNS browse — use a Capacitor plugin to browse Bonjour services on the LAN (preferred, zero friction)
  2. QR code — the machine already exposes GET /api/v1/wifi/config/qr.png; scan it at first launch
  3. Manual IP entry — fallback, already supported via config.json mechanism

Note: the existing install script already solves mDNS discovery — port that logic.

Command Mapping

MachineService method Machine native call
start() GET /api/v1/action/start
stop() GET /api/v1/action/stop
abort() GET /api/v1/action/abort
tare() GET /api/v1/action/tare
preheat() GET /api/v1/action/preheat
purge() GET /api/v1/action/purge
loadProfile(id) GET /api/v1/profile/load/{id}
listProfiles() GET /api/v1/profile/list?full=true
getHistory() POST /api/v1/history
subscribe(listener) Socket.IO status + sensors events

Telemetry (Socket.IO)

Connect to http://meticulous-{id}.local/socket.io/. Subscribe to status and sensors events, map to the existing MachineState shape used by the web app.

AI Profile Generation

Currently handled server-side (Python calls Gemini). In this adapter:

  • User supplies their own Gemini API key (entered in app settings)
  • Image analysis and profile generation are called directly from the app
  • The existing server-side logic is the reference implementation

Part 2: Capacitor Setup

apps/
├── web/          # React source (unchanged)
└── ios/          # NEW: Capacitor Xcode project
    ├── App/
    │   ├── App/          # Swift app entry
    │   └── public/       # Symlink → ../../web/dist
    └── capacitor.config.ts

capacitor.config.ts

const config: CapacitorConfig = {
  appId: 'com.meticai.app',
  appName: 'MeticAI',
  webDir: '../web/dist',
  server: {
    // In dev: point to vite dev server
    // In prod: use bundled web app
  },
}

Info.plist additions

<key>NSLocalNetworkUsageDescription</key>
<string>MeticAI needs local network access to communicate with your Meticulous espresso machine.</string>
<key>NSBonjourServices</key>
<array>
  <string>_meticulous._tcp</string>
</array>

Build script (apps/ios/scripts/build.sh)

#!/usr/bin/env bash
# 1. Build web app
cd ../web && bun run build
# 2. Sync to Capacitor
cd ../ios && npx cap sync ios
# 3. Open Xcode (or use xcodebuild for CI)
npx cap open ios

Part 3: App Store Preparation

  • Apple Developer account + bundle ID (com.meticai.app)
  • Privacy manifest (PrivacyInfo.xcprivacy) — local network, no tracking
  • App icon set (assets already exist in web app public folder)
  • LaunchScreen storyboard
  • Screenshots (iPhone 15 Pro, iPad Pro)
  • App Store description
  • TestFlight beta distribution before public release

Part 4: Features Scope

In scope (v1.0 iOS)

  • Machine discovery (mDNS + manual IP fallback)
  • Live telemetry view
  • Profile list and selection
  • Shot start / stop / abort / tare / preheat
  • Shot history browser
  • AI profile generation (user's own Gemini key)
  • Pour over mode
  • Settings (machine IP, Gemini API key, preferences)

Out of scope (post-v1.0)

  • Scheduling / recurring shots (server feature, no direct machine equivalent)
  • Home Assistant / MQTT integration (server feature)
  • Remote access via Tailscale (server feature — users who want remote access run the Pi stack)
  • Android

What Stays On The Pi (Docker)

The Pi stack is not replaced — it continues to serve:

  • Home Assistant integration (MQTT)
  • Remote access via Tailscale
  • Advanced scheduling
  • Web browser access

The iOS app is an alternative client for users who don't want to (or can't) run a Pi.

Dependencies

  • #252 MachineService abstraction layer (hard prerequisite)
  • Capacitor 7.x (@capacitor/core, @capacitor/ios, @capacitor/camera)
  • Capacitor community network plugin for mDNS browsing (or native Swift plugin)
  • Apple Developer Program membership
  • Xcode 16+

Acceptance Criteria

  • MachineDirectAdapter implements the full MachineService interface
  • Machine discovered automatically on launch (mDNS) with manual IP fallback
  • Live telemetry updates in real time via Socket.IO
  • All primary machine commands work (start, stop, tare, preheat, purge, load profile)
  • AI profile generation works with user-supplied Gemini API key
  • Shot history accessible
  • App passes App Store review
  • No changes required to the existing web app or Python server
  • ipa file automatically generated in CI/CD build process

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions