# Tybe — Mobile client spec (iOS + Android)

This is the cross-platform contract for the iOS and Android Tybe client
apps. It is the single source of truth shared by both platform dev guides
([`docs/clients/ios.md`](ios.md), [`docs/clients/android.md`](android.md))
and is what the orchestrator hands to any subagent building a mobile
client.

The iOS and Android apps are functionally identical to the Mac GUI client
(`clients/swift-shared/Sources/tybe-client`). They speak the same Tybe BLE
GATT contract documented in [`docs/protocol.md`](../protocol.md), produce
the same opcode stream, and connect to the same bridge advertising as
`Tybe Zephyr Dev`.

> **Reference implementation:** the Mac client and the
> [`TybeProtocol`](../../clients/swift-shared/Sources/TybeProtocol/),
> [`TybeHangul`](../../clients/swift-shared/Sources/TybeHangul/), and
> [`TybeBLE`](../../clients/swift-shared/Sources/TybeBLE/) Swift
> packages. Read those before writing a new platform port; the wire
> format and the planner behavior must match exactly.

## 1. Product surface

The mobile clients deliver, in order of priority:

1. **Live keyboard over BLE** — type on the phone, keys appear on a
   target host the dongle is plugged into. Per-keystroke latency target:
   under 100 ms perceived.
2. **Compose mode** — type or paste a buffer (Korean + ASCII), tap send,
   the whole string streams to the target as a sequence of frames. The
   target host's input source is user-managed; clients do not expose a
   language-switch control.
3. **Live status** — show whether the BLE link is up, whether the bridge
   is healthy, and whether the dongle is still ACK'ing. These map to the
   Status characteristic flags described below.

Voice / STT input, on-device LLM cleanup, and bluetooth bonding are
**out of scope for v1**. We can add them once the keyboard path is solid
on both platforms.

## 2. UX contract

Both platform UIs should expose the same elements. They do not have to
match visually, but the affordances and behaviors must be equivalent so
the docs and screenshots from the Mac client carry over.

### 2.1 Top-level layout

- **Header**
  - App title.
  - Mode picker: `Keyboard` / `Compose`. Default: Keyboard.
  - Bridge name field (default: `Tybe Zephyr Dev`, persisted across
    launches).
  - Connect button — opens the BLE link without sending a frame.
  - Reset button — drops the persistent BLE connection.
  - Show-Log toggle.

- **Status bar (always visible)**
  - BLE link pill: `idle / connecting / connected / failed`.
  - Bridge pill: `Bridge: OK` (green) / `Bridge: <error>` (red) / `Bridge: —` (grey before any traffic).
  - Dongle pill: `Dongle: linked` (green) / `Dongle: no ACK` (red) / `Dongle: —` (grey).
  - Counters: total keys, total frames, total errors. Errors are
    highlighted red when non-zero.

- **Main pane**: depends on mode (keyboard / compose).

- **Footer**: optional status/error summary. In compose mode, include a
  Send button.

- **Log pane (toggleable)**: rolling, timestamped, color-coded `INFO /
  OK / WARN / ERROR` entries. Shows raw bridge status descriptions and
  any BLE errors. Limited to the last ~600 entries; clearable.

### 2.2 Keyboard mode

- Big "last key" glyph using the same human label format as the Mac
  client (modifier glyphs `⌃ ⌥ ⇧ ⌘`, special key labels: `↩ esc ⌫ ⇥ ␣ ←
  → ↑ ↓ ⇪`, lowercase letters, digits).
- Recent-key strip: the last ~20 keys as small chips, scrollable
  horizontally.
- A text field or transparent capture area below that focuses
  immediately and absorbs every keypress as a single frame.
- Hardware modifier behavior (when present): `Cmd / Ctrl / Alt /
  Shift` produce HID modifier bits attached to the keycode in the same
  frame.
- On the on-screen software keyboard:
  - Each character insert becomes one `KEY` opcode (or jamo run for
    Korean).
  - Backspace becomes `KEY 0x2A` (HID Backspace).
  - Enter becomes `KEY 0x28` (HID Enter).
  - Special keys not on the soft keyboard (Tab, Esc, arrow keys) need
    a small toolbar (icons in the keyboard pane) to send `KEY 0x2B`
    (Tab), `0x29` (Esc), `0x4F-0x52` (arrows).
### 2.3 Compose mode

- Multi-line text editor.
- Send button: chunks planner output into frames and sends them on the
  same persistent BLE connection. Status updates per frame should be
  visible in the log pane.
- The host input source is user-managed; clients should not emit
  `TOGGLE_INPUT_SOURCE` or expose auto-toggle controls.

### 2.4 Settings (or equivalent)

- Bridge name (also editable from the header).
- Show-log default.
- "Reset BLE on app foreground" — convenience toggle for users who
  switch hosts often.

## 3. BLE / GATT contract

Authoritative spec lives in [`docs/protocol.md`](../protocol.md). What a
mobile client must do:

### 3.1 Service discovery

- Scan for advertising name **`Tybe Zephyr Dev`** (configurable).
- Connect.
- Discover the Tybe service:

  ```text
  Service:     74796265-0000-0001-8000-00805F9B34FB
  Text Input:  74796265-0010-0001-8000-00805F9B34FB  (write, write-without-response)
  Status:      74796265-0020-0001-8000-00805F9B34FB  (notify, read)
  Config:      74796265-0030-0001-8000-00805F9B34FB  (read, write)
  Control:     74796265-0040-0001-8000-00805F9B34FB  (write, write-without-response)
  ```

- Subscribe to Status notifications immediately after connect.
- Optional: read Config to know the bridge's firmware version, ESB
  address, and protocol version.

### 3.2 Frame format (Text Input characteristic)

```text
+--------+--------+----------------+
| seq[1] | len[1] | opcodes[len]   |
+--------+--------+----------------+
```

- `seq` increments per frame, wraps at 256. Used by the dongle for
  duplicate suppression.
- `len` is the count of opcode bytes (0–253). Maximum frame on the
  wire is **255 bytes**.
- One or more frames per user action. The Mac client splits long
  strings using `TybeFrameBuilder.chunk(opcodes:startingSeq:)` — port
  this logic verbatim.

### 3.3 Status characteristic format (4 bytes)

```text
+--------+--------+--------+--------+
| flags  |  seq   | error  | rsv    |
+--------+--------+--------+--------+
```

- `flags`:
  - bit 0: bridge ready (always set when bridge sends a status).
  - bit 1: ACK received for this frame.
  - bit 2: dongle linked (sticky; cleared after ~4 s of no ACK).
- `seq`: the seq this status describes (or the bridge's last seq for
  heartbeats).
- `error`: 0 = OK; otherwise see the error table in
  [`docs/protocol.md`](../protocol.md).

The bridge auto-emits status notifications:

- on every Text Input frame,
- on the 2 s heartbeat ping,
- on Control characteristic write `\x01` (request status),
- on a fresh BLE connection.

So clients should *only* update UI in response to notifications, not
poll.

### 3.4 Sending behavior

- Use `Write Without Response` for frames in keyboard mode (low
  latency).
- Use `Write With Response` is acceptable in compose mode if
  per-frame backpressure is desired, but `Write Without Response` +
  status acknowledgements is better.
- Keep the BLE connection alive across keystrokes. **Do not**
  disconnect/reconnect per frame — the Mac client used to do that and
  it is too slow for live keyboard.
- On any write error or notification timeout: drop the connection,
  clear `connectedName`, and reconnect on the next user action. The
  Mac client's `LiveBLESender` is the reference pattern.

### 3.5 Error handling

- BLE scan timeout → surface as `BLE: failed`, log entry, do not
  crash.
- Bridge status with `error != 0` → log warn, increment error
  counter, surface description. Don't auto-disconnect.
- Notification with `dongleLinked == 0` → flip the dongle pill red.
- Notification with `dongleLinked == 1` → flip back green.

## 4. Opcode planner (must port)

The planner converts user input (a string for compose mode, individual
keypresses for keyboard mode) into a Tybe opcode stream.

Reference Swift implementation: `clients/swift-shared/Sources/TybeHangul/`
and `clients/swift-shared/Sources/TybeProtocol/`.

### 4.1 ASCII keymap

Each printable ASCII character maps to a `(HIDKey, HIDModifiers)` tuple
on the US-QWERTY layout. See
[`AsciiKeymap.swift`](../../clients/swift-shared/Sources/TybeProtocol/AsciiKeymap.swift)
for the full table. Lowercase letters use `KEY hid_kc=0x04..0x1D, mod=0`.
Uppercase letters add `leftShift = 0x02`. Symbols (`! @ # $ % ^ & *` etc.)
use Shift + the appropriate key. Non-printable special keys (Enter, Tab,
Backspace, etc.) live in
[`HIDKey.swift`](../../clients/swift-shared/Sources/TybeProtocol/HIDKey.swift).

### 4.2 Hangul decomposition

Pre-composed Hangul syllables in the Unicode block `U+AC00..U+D7A3` are
decomposed into a `(choseong, jungseong, jongseong)` triple using TR 15
arithmetic:

```text
index = scalar - 0xAC00
choseong = index / 588
jungseong = (index % 588) / 28
jongseong = index % 28
```

The triple is then mapped to a 2-set QWERTY keystroke sequence using
the table in
[`KoreanKeymap.swift`](../../clients/swift-shared/Sources/TybeHangul/KoreanKeymap.swift).
Each jamo becomes one or two ASCII keystrokes.

### 4.3 Input-source behavior

Clients use manual planner behavior and emit no input-source toggles.
The focused host's input source is user-managed. `TOGGLE_INPUT_SOURCE`
may remain in the wire protocol for compatibility, but iOS, Android,
macOS, and CLI clients should not expose it or call it.

### 4.4 Frame chunking

After the planner produces a flat opcode array, chunk into frames of
≤253 opcode bytes per frame. Maintain a monotonic `seq` byte. See
`TybeFrameBuilder.chunk` in
[`TybeBLE.swift`](../../clients/swift-shared/Sources/TybeBLE/TybeBLE.swift).

### 4.5 Test vectors

Both ports must round-trip
[`protocol/test-vectors.json`](../../protocol/test-vectors.json):

```json
{
  "input": "안녕하세요 hello",
  "expected_opcodes": [...],
  "expected_frames": [...]
}
```

A passing test of all vectors is a release blocker.

## 5. Live keyboard — input capture

Capturing every keystroke from the on-screen keyboard is the core UX
challenge per platform.

### Common rules

- Each captured event becomes exactly one Tybe frame.
- The frame contains one `KEY` opcode (or for Korean syllables, the
  jamo expansion of the new character).
- Special keys: backspace, enter, tab, esc, arrows have explicit
  HIDKey codes; map them.
- Auto-repeat (holding a key): out of scope for v1. Tap = one frame.
  Mention this in the UI ("hold-to-repeat is not implemented yet").

### iOS

A `UITextView`/`SwiftUI.TextField` exposes per-character changes via
`UITextViewDelegate.textViewDidChange` or `.onChange(of:)`. The diff
between `oldValue` and `newValue` is the keystroke. See
[`docs/clients/ios.md`](ios.md) for the chosen approach.

### Android

A custom `EditText` with an `InputFilter` and a
`TextWatcher` works but is fiddly; alternatively listen to the
`InputConnection` via a custom `View` with an `onCreateInputConnection`
override. See [`docs/clients/android.md`](android.md).

## 6. Permissions & background behavior

| Concern | iOS | Android |
|---|---|---|
| BLE scan permission | `NSBluetoothAlwaysUsageDescription` in `Info.plist`; runtime prompt on first scan | `BLUETOOTH_SCAN`, `BLUETOOTH_CONNECT` runtime permissions on Android 12+; `ACCESS_FINE_LOCATION` on Android 11 and below |
| BLE in background | `bluetooth-central` background mode in `Info.plist`; reduced scan but writes/notifications keep working | Foreground service with FGS type `connectedDevice` for sustained sessions; or accept that BLE only runs while the app is in foreground |
| Mic permission | not needed in v1 | not needed in v1 |
| Network permission | not needed in v1 | not needed in v1 |
| Battery optimization | n/a | document that the user should exempt the app from battery optimization for reliable background BLE |

For v1 we accept "BLE only while app is foreground" on both platforms
to keep the scope minimal. Background reconnect can be added later.

## 7. Acceptance criteria

A mobile client implementation is considered done when:

1. The app builds reproducibly from the repo with the platform's
   default toolchain (Xcode for iOS, Gradle for Android).
2. It connects to a `Tybe Zephyr Dev` bridge via BLE on first launch
   without requiring system Bluetooth pairing.
3. The Status pill shows `Bridge: OK` and `Dongle: linked` within 5 s
   of a successful connect, driven by real Status notifications, not
   inference.
4. Live typing works: on-screen keyboard or hardware keyboard
   characters appear typed on the dongle's USB host with under 100 ms
   perceived latency for ASCII.
5. Compose mode types Korean correctly: sending `안녕하세요` with the
   target host's Korean input source enabled produces `안녕하세요` on
   the target.
6. Cross-language test vectors in `protocol/test-vectors.json` round-
   trip through the platform's planner port.
7. Disconnecting the dongle's USB plug flips the Dongle pill red
   within ~4 s; reconnecting flips it green within ~2 s (the bridge's
   heartbeat cadence).
8. The log pane shows a reasonable trace of activity without flooding
   the UI.
9. App rotation, backgrounding, and re-foregrounding do not crash the
   app or leak BLE connections.

## 8. Non-goals (v1)

- STT / voice input.
- Multiple bridges.
- Bonding / encrypted GATT.
- Custom keymaps (per-host layout).
- Macros / snippets.
- Cloud sync of settings.
- HID-over-GATT (HOGP) emulation.

## 9. Repo layout

```text
clients/
├── swift-shared/   # macOS GUI + shared SwiftPM (already exists)
├── ios/            # SwiftUI app — see docs/clients/ios.md
└── android/        # Compose / Kotlin app — see docs/clients/android.md
```

Each new client lives in its own subdirectory and depends only on the
protocol contract + the platform's BLE stack. Code reuse with
`swift-shared/` is welcome on iOS but not required.

## 10. Subagent handoff

The orchestrator should hand a subagent the following bundle:

1. This document.
2. [`docs/protocol.md`](../protocol.md).
3. [`docs/korean-strategy.md`](../korean-strategy.md).
4. The platform-specific dev guide:
   [`docs/clients/ios.md`](ios.md) or
   [`docs/clients/android.md`](android.md).
5. Read access to `clients/swift-shared/` as the reference
   implementation.
6. Read access to `firmware/bridge-zephyr/` so the subagent can
   inspect the bridge for any contract details not yet documented.

Subagents must propose any protocol or behavior change as a PR
amending `docs/protocol.md` and this spec, not as a quiet client-side
divergence.
