# Tybe Protocol — v0.1 (draft)

The contract between client apps, the bridge, and the dongle. Anything in this doc is consumed by all four parties; changes here are breaking and require coordinated updates.

## Layers

```
[client app]   → opcode stream as bytes
                                                  ┐
[bridge BLE]   → GATT writes to Text Input        │ shared opcode bytes
[bridge ESB]   → ESB packets to dongle            │ are passed through
[dongle USB]   → executes opcodes, sends HID      ┘
```

The opcode stream is the single source of truth. Bridge does not reinterpret. Dongle is the only consumer.

## BLE GATT service

Development service UUID: `74796265-0000-0001-8000-00805F9B34FB`. The first 32 bits are ASCII `tybe` for bring-up readability. Regenerate these UUIDs before public release.

| Characteristic | Development UUID | Properties | Description |
|---|---|---|---|
| Text Input | `74796265-0010-0001-8000-00805F9B34FB` | Write, Write Without Response | Opcode stream chunks. Each write is a complete or partial frame; framing rules below. |
| Status | `74796265-0020-0001-8000-00805F9B34FB` | Notify | Bridge → client status updates: dongle link state, last-acked sequence, errors. |
| Config | `74796265-0030-0001-8000-00805F9B34FB` | Read, Write | Bridge configuration: firmware version, ESB address, opcode protocol version. |
| Control | `74796265-0040-0001-8000-00805F9B34FB` | Write | Out-of-band commands to bridge: reset link, request status, enter pairing mode. |

### Text Input framing

Each BLE write is one **frame**:

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

- `seq` — 8-bit sequence number, increments per frame, wraps at 256. Used to dedupe retries.
- `len` — payload length (0..253). Max frame = 255 bytes (fits in default ATT MTU 247 with overhead room).
- `opcodes` — concatenated opcodes, no padding.

Bridge ACKs each frame by emitting a Status notification with `last_acked_seq = seq` once the dongle confirms reception over ESB.

### Status notification format

```
+--------+--------+--------+--------+
| flags  | seq[1] | err[1] | rsv[1] |
+--------+--------+--------+--------+
```

- `flags`:
  - bit 0 = bridge ready (set on every status notification)
  - bit 1 = ACK received for this frame (the bridge successfully heard the dongle's radio ACK for the frame this status describes; clear on a heartbeat or queue-full status)
  - bit 2 = dongle linked (sticky: set while a dongle radio ACK has been received within the last few seconds; cleared by a heartbeat that sees no ACK)
  - bits 3..7 = reserved
- `seq`: last successfully acked Text Input frame seq.
- `err`: last error code (0 = none; codes TBD).
- `rsv`: reserved.

### Config format (Read)

```
+--------+--------+--------+----------------+
| ver[2] | proto  | rsv[1] | esb_addr[5]    |
+--------+--------+--------+----------------+
```

- `ver`: bridge firmware version (major, minor).
- `proto`: opcode protocol version (currently 1).
- `esb_addr`: 5-byte ESB address.

## Opcode set v1

Each opcode is a one-byte type followed by a fixed-length payload. No length prefix per opcode — the opcode type determines payload length.

### `KEY` (0x10) — emit a single key with optional modifiers

```
+--------+--------+--------+
|  0x10  | hid_kc | hid_md |
+--------+--------+--------+
```

- `hid_kc` — USB HID Boot Keyboard scancode (Usage Page 0x07). Examples: `0x04` = A, `0x28` = Enter, `0x2C` = Space.
- `hid_md` — USB HID modifier byte. Bits: 0x01 LCtrl, 0x02 LShift, 0x04 LAlt, 0x08 LGUI, 0x10 RCtrl, 0x20 RShift, 0x40 RAlt, 0x80 RGUI.

Dongle behavior: emit `hid_md` in the modifier field of an HID report, set `hid_kc` in the first keycode slot, send report. Then send a key-up report (modifier 0, all keycodes 0). This pulse is one keystroke.

### `KEY_DOWN` (0x11) — press a key down with optional modifiers

```
+--------+--------+--------+
|  0x11  | hid_kc | hid_md |
+--------+--------+--------+
```

- `hid_kc` — USB HID Boot Keyboard scancode (Usage Page 0x07).
- `hid_md` — USB HID modifier byte.

Dongle behavior: Add `hid_md` to the active modifier mask and append `hid_kc` to the active pressed key list. Emit the keyboard report. This key is held pressed.

### `KEY_UP` (0x12) — release a pressed key

```
+--------+--------+--------+
|  0x12  | hid_kc | hid_md |
+--------+--------+--------+
```

- `hid_kc` — USB HID Boot Keyboard scancode (Usage Page 0x07).
- `hid_md` — USB HID modifier byte.

Dongle behavior: Remove `hid_md` from the active modifier mask and remove `hid_kc` from the active pressed key list. Emit the keyboard report. This key is released.

### `DELAY` (0x20) — wait N milliseconds before next opcode

```
+--------+--------+--------+
|  0x20  | ms_lo  | ms_hi  |
+--------+--------+--------+
```

- `ms` — little-endian uint16, milliseconds. Range 0..65535. Typical use: 5-50ms between syllables to give the host IME time to compose.

Reserved for v1.1+; v1 dongle may treat as no-op (the default 5ms inter-key spacing is usually enough).

### `TOGGLE_INPUT_SOURCE` (0x30) — emit configured input-source-switch hotkey

```
+--------+
|  0x30  |
+--------+
```

The actual hotkey emitted is configured per-bridge via the Config characteristic (TBD, v1.1). The dongle stores the hotkey as a (modifier, scancode) pair and emits it as a single keystroke. Default in v1: emits Caps Lock (`0x39`) which is the recommended Korean toggle on macOS.

### `SET_TOGGLE_HOTKEY` (0x31) — configure the hotkey used for language toggle

```
+--------+--------+--------+
|  0x31  | hid_kc | hid_md |
+--------+--------+--------+
```

- `hid_kc` — USB HID Boot Keyboard scancode (Usage Page 0x07). Default: `0x39` (Caps Lock).
- `hid_md` — USB HID modifier byte. Default: `0`.

Dongle behavior: Update internal memory with `hid_kc` and `hid_md`. Any subsequent `TOGGLE_INPUT_SOURCE` (0x30) opcodes will emit this configured hotkey instead of the default. This allows client apps to configure target compatibility (e.g. Windows Right Alt, Android Shift+Space, etc.) on the fly.

## Sequencing and reliability

- BLE writes use Write Without Response by default for throughput. The Status notification provides app-level acks.
- Within a frame, opcodes execute strictly in order.
- Frames are processed in `seq` order. If the bridge sees a gap, it requests retransmission via the Status `err` field (mechanism TBD).
- The dongle does not buffer beyond ~16 frames. Bursts above that get dropped at the ESB layer; clients must throttle to status acks.

## ESB transport (bridge ↔ dongle)

- 2 Mbps, channel 76, 5-byte shared address (factory or pairing-flashed).
- Auto-ack on, 3 retries, 250 µs retry delay.
- Payload: same byte stream as a BLE Text Input frame (seq, len, opcodes), wrapped in ESB packet.
- Dongle replies with a 1-byte ack containing `seq`.

## Versioning

`Config.proto` is the single version field. Clients read it on connect:
- `1` (v1.0): KEY, DELAY (no-op), TOGGLE_INPUT_SOURCE.
- `2`+ (v1.1+): mouse/scroll opcodes (see "Reserved opcodes" below) without breaking v1 dongles since v1 dongles ignore unknown opcode types.

Dongles must skip unknown opcodes by reading the first byte and consulting a length table built into firmware. Lengths are fixed per opcode type; if a future opcode adds variable length, it must use a sub-length byte.

## Reserved opcodes (v1.1+)

These are not implemented in v1 but the type bytes are reserved so v1 client / firmware code that ignores unknown opcodes will keep working when v1.1 ships.

### `MOUSE_MOVE` (0x40)
```
+--------+--------+--------+
|  0x40  |  dx    |  dy    |
+--------+--------+--------+
```
- `dx`, `dy` — signed int8, relative cursor delta in pixels (-128..127).
- Larger moves split into multiple opcodes by the client.

### `MOUSE_BUTTON` (0x41)
```
+--------+--------+
|  0x41  | mask   |
+--------+--------+
```
- `mask` — bit0 left, bit1 right, bit2 middle. Bits set = pressed; bits cleared = released. Stateful: dongle holds the last value until next `MOUSE_BUTTON`.

### `MOUSE_SCROLL` (0x42)
```
+--------+--------+--------+
|  0x42  |  v     |  h     |
+--------+--------+--------+
```
- `v`, `h` — signed int8 scroll lines, vertical and horizontal.

### `RAW_HID` (0x80)
```
+--------+--------+----------------+
|  0x80  | len[1] | report[len]    |
+--------+--------+----------------+
```
- Variable-length escape hatch for arbitrary HID reports. Use sparingly; defeats the dongle's input-validation story.

These are sketches — the v1.1 spec freeze happens when we actually build mouse / trackpad support. Until then the type bytes 0x40-0x4F and 0x80 are off-limits for any other use.

## Test vectors

Canonical input → opcode-bytes pairs live in `protocol/test-vectors.json`. All clients and the dongle simulator must round-trip these. Tests run on every commit.

## Open questions

- Service UUID — pick before public release.
- Pairing protocol — v1 ships with a hardcoded ESB address; v1.1 adds a button-press pairing flow.
- Error code table — fill in once we hit real failure modes during Phase 2 firmware bring-up.
- TOGGLE_INPUT_SOURCE configuration — should the hotkey live in dongle Config (one bridge, all hosts use same toggle) or be sent inline (more flexible, more bytes)? Decide in Phase 2.
