---
type: reference
title: "CAMDOM -- BLE Packet Protocol"
description: "Custom binary protocol for BLE mesh communication: connection, session, disconnection, alarm, migration, sound, liveness packets."
tags: [camdom, ble, protocol, packets, mesh, security]
timestamp: "2026-07-20"
---

# CAMDOM -- BLE Packet Protocol Reference

## Protocol Overview

CAMDOM uses a custom text-based packet protocol over BLE (Bluetooth Low Energy) GATT characteristics for inter-device communication. The protocol enables a **server-client** architecture where one device acts as the GATT peripheral (server) and all others connect as clients.

### Design Rationale

- **No internet dependency**: All state is peer-to-peer over BLE. Works offline, in clubs, parties, anywhere.
- **Minimal packet size**: Packets are Base64-encoded strings sent over a 53-byte MTU. Each packet is `{type}:{payload}` to minimize overhead.
- **Server-authoritative model**: The server is the source of truth. Clients never act unilaterally -- they request, the server decides.
- **Hold-to-disconnect**: Disconnect and alarm-stop require a press-and-hold gesture. This prevents accidental disconnection and ensures all parties agree before separation.
- **Platform encoding in advertisement**: iOS devices advertise values 1-124, Android devices advertise 126-249. Value 125/251 means "becoming server". This avoids explicit role negotiation -- the device that has been a server longer wins.

### BLE Characteristics

| Characteristic | UUID | Role |
|---|---|---|
| Service | `25AE1441-05D3-4C5B-8281-93D4E07420CF` | GATT Service |
| Read | `25AE1442-05D3-4C5B-8281-93D4E07420CF` | Client reads |
| Write | `25AE1443-05D3-4C5B-8281-93D4E07420CF` | Client writes to server |
| Indicate | `25AE1444-05D3-4C5B-8281-93D4E07420CF` | Server indicates to client |

### Transport Encoding

All packets are **Base64-encoded** (`btoa`/`atob`). The raw text format is `{type}:{payload}` or `{type}:{action}|{data}`.

---

## Packet Format Reference

### Connection Handshake -- `c:`

**Purpose**: First packet sent by client to server after BLE connection. The server echoes it back to confirm the round-trip.

**Format**: `c:{platform}:{advertisedValue}`

| Field | Values | Meaning |
|---|---|---|
| platform | `0` | iOS |
| platform | `1` | Android |
| advertisedValue | 1-249 | The random value this device was advertising |

**Examples**:

```
c:0:87        # iOS device, was advertising value 87
c:1:132       # Android device, was advertising value 132
```

**Flow**:
1. Client connects to server over BLE
2. Client sends `c:{platform}:{value}` to server via CHAR_FOR_WRITE_UUID
3. Server echoes the exact same packet back to the client via CHAR_FOR_INDICATE_UUID
4. Client receives the echo, confirms round-trip complete, transitions to `"connected"` state
5. Client then sends session request `s:r|{fireOnDisconnect}`

**Edge case**: If the received echo does NOT match `connectionPacketSent` (the original packet), it is ignored silently. This prevents cross-client echo confusion.

---

### Session Sync -- `s:`

**Purpose**: Establishes the session. Server generates a UUID-based session ID and distributes it to all clients. Also synchronizes the "fire alarm on disconnect" preference.

**Format**: `s:r|{fireOnDisconnect}` (request) / `s:a|{sessionId}` (answer)

**Request** (`s:r|{fireOnDisconnect}`) -- Client to Server:

| Field | Values | Meaning |
|---|---|---|
| fireOnDisconnect | `0` or `1` | Whether client wants alarm to fire if this client disconnects |

**Example**:

```
s:r|1          # Client wants alarm to fire on disconnect
s:r|0          # Client does NOT want alarm to fire on disconnect
```

**Answer** (`s:a|{sessionId}`) -- Server to Client:

| Field | Values | Meaning |
|---|---|---|
| sessionId | 8-char UUID string | Unique session identifier, generated on first client connection |

**Example**:

```
s:a|a3f2b8c1   # Session ID assigned
```

**Flow**:
1. Client receives `c` echo, transitions to `"connected"`
2. Client sends `s:r|{0|1}` with its fireOnDisconnect preference
3. Server receives, stores the preference (if any client wants fire, `hasAnyoneRequestedToFireWhenDisconnect = true`)
4. Server sends `s:a|{sessionId}` to the requesting client only (unicast, not broadcast)
5. Client receives session ID, transitions to `"synced"` state, begins RSSI monitoring

**Server-side behavior**: If `connectedClients.length >= 1`, server transitions to `"synced"` state and starts the proximity sensor timeout (3.5s delay before enabling RSSI monitoring).

---

### Disconnection Protocol -- `d:`

**Purpose**: Negotiated disconnection requiring mutual consent. The "hold-to-disconnect" gesture means both parties must agree before the BLE connection is severed.

**Format**: `d:{type}|{action}`

#### Disconnection Request -- `d:r|`

| Packet | Direction | Meaning |
|---|---|---|
| `d:r\|s` | Client to Server | **Start** -- Client has started holding the disconnect button |
| `d:r\|f` | Client to Server | **Finish** -- Client has released the disconnect button (held long enough) |
| `d:r\|1` | Client to Server | **Confirm** -- Client confirms it is ready to disconnect (after server sends `d:a\|a`) |

#### Disconnection Answer -- `d:a|`

| Packet | Direction | Meaning |
|---|---|---|
| `d:a\|a` | Server to Client | **Accepted** -- Server accepts the disconnection request, client should proceed |
| `d:a\|r` | Server to Client | **Rejected** -- Server rejects the disconnection (e.g., during alarm negotiation) |
| `d:a\|a1` | Server to Client(s) | **Final Accept** -- Broadcast to ALL clients: disconnect now, session is over |

#### Edge Case Packet -- `d:r|ff`

| Packet | Direction | Meaning |
|---|---|---|
| `d:r\|ff` | Client to Server | **Force Finish** -- Used when client is in the `isAmIClientAllowedToDisconnectEdgeCase` state (e.g., during alarm with hold-to-disconnect shortcut) |

**Full Disconnection Flow**:

```mermaid
sequenceDiagram
    participant C as Client
    participant S as Server

    Note over C: User presses disconnect
    C->>S: d:r|s
    Note over S: Client added to askedDisconnectionDevices
    Note over S: Polling: waiting for all clients to ask

    Note over C: User releases disconnect
    C->>S: d:r|f
    Note over S: Remove client from askedDisconnectionDevices
    Note over S: Check if fireOnDisconnect applies

    alt All clients requested AND server also wants disconnect
        Note over S: askedDisconnectionDevices.length == connectedClients.length
        S->>C: d:a|a (unicast to each client)
        C->>S: d:r|1
        S->>C: d:a|a1 (broadcast to all)
        Note over C: Client disconnects from server
    else Server not ready yet
        Note over S: Wait for more clients
    end
```

**Server-side disconnection logic** (500ms polling interval):

1. Server checks every 500ms if `askedDisconnectionDevices.length === connectedClients.length`
2. If ALL clients have requested AND server button is still held (`isDisconnectButtonPressed`):
   - Sets `serverAlreadyShouldDisconnect = true`
   - Sends `d:a|a` to each client that requested
   - Clears alarm state
3. If `connectedClients.length === 0` (all already disconnected):
   - Plays "unpair" sound, haptic feedback
   - Transitions to `"disconnected"` state
   - Stops advertising, clears all state

**Edge case -- orphaned server**: If the server releases before all clients respond, and some clients have already disconnected, the server checks `connectedClients.length === 0` and cleans up immediately.

---

### Alarm System -- `a:`

**Purpose**: Security alarm that fires when a connected device is taken away or goes to background. Requires consensus to stop -- "everyone must agree."

**Format**: `a:{type}|{action}|{data}`

#### Alarm Fire -- `a:f|`

| Packet | Direction | Meaning |
|---|---|---|
| `a:f\|1\|u` | Server to Client(s) | **Fire alarm -- user-triggered** (server went to background) |
| `a:f\|1\|a` | Server to Client(s) | **Fire alarm -- disconnect-triggered** (disconnection while fireOnDisconnect is active) |
| `a:f\|0\|a` | Server to Client(s) | **Stop alarm** -- all clients agreed to stop |

#### Alarm Request -- `a:r|`

| Packet | Direction | Meaning |
|---|---|---|
| `a:r\|s` | Client to Server | **Start** -- Client wants to stop alarm (started holding button) |
| `a:r\|f` | Client to Server | **Finish** -- Client released alarm-stop button |
| `a:r\|i` | Client to Server | **Initiate alarm** -- Client went to background (triggers alarm on server) |
| `a:r\|ff` | Client to Server | **Force finish** -- Edge case: client in `isAmIClientAllowedToDisconnectEdgeCase` |

#### Alarm Update -- `a:u|`

| Packet | Direction | Meaning |
|---|---|---|
| `a:u\|i` | Client to Server | **Initiate** -- Client went to background, server should broadcast alarm |
| `a:u\|rssi\|{value}\|{sense}` | Client to Server | **RSSI proximity data** (see RSSI section) |

#### Alarm Trigger Sources

1. **App backgrounding** (server goes to background): Server broadcasts `a:f|1|u`. On Android, also fires alarm sound locally.
2. **App backgrounding** (client goes to background): Client sends `a:r|i` to server. Server broadcasts `a:f|1|u` to all.
3. **Disconnection with fireOnDisconnect**: When server processes `d:r|f` and `hasAnyoneRequestedToFireWhenDisconnect` or `StorageKeys.fireAlarmOnDisconnect` is true, server broadcasts `a:f|1|a`.
4. **Disconnection edge case**: When `d:r|1` is received but client was NOT in `askedDisconnectionDevices`, alarm fires with `a:f|1|a`.

**Stop Alarm Flow**:

```mermaid
sequenceDiagram
    participant C1 as Client 1
    participant C2 as Client 2
    participant S as Server

    Note over S: Alarm is firing (a:f|1|a broadcast)
    C1->>S: a:r|s (start holding alarm button)
    Note over S: Client 1 added to askedToStopAlarmClients

    C2->>S: a:r|s (start holding alarm button)
    Note over S: Client 2 added to askedToStopAlarmClients

    Note over S: Check: askedToStopAlarmClients.length == connectedClients.length?
    Note over S: AND isAlarmButtonPressed?

    alt All clients agreed AND server also holding
        S->>C1: a:f|0|a (broadcast stop)
        S->>C2: a:f|0|a (broadcast stop)
        Note over S: Alarm stops, state -> "disconnected"
    else Not all clients agreed yet
        Note over S: Keep waiting
    end
```

**Critical behavior**: The server itself must ALSO be holding the alarm-stop button (`isAlarmButtonPressed`). If only clients want to stop but the server does not, the alarm continues. This is the "everyone must agree" model -- the server is a participant, not just a relay.

---

### Connection Migration -- `m:`

**Purpose**: When a new server with a higher advertised value appears, the current server tells all clients to disconnect and reconnect to the new server. This handles the case where a more senior device (longer-serving server) returns.

**Format**: `m:d|{targetValue}` or `m:d|0`

| Packet | Direction | Meaning |
|---|---|---|
| `m:d\|{targetValue}` | Server to Client(s) | **Migrate** -- Disconnect and reconnect to device advertising `targetValue` |
| `m:d\|0` | Server to Client(s) | **Abort migration** -- The migration target is no longer valid |

**Migration Trigger**: During scanning, if a client discovers a device with a higher advertised value AND the client is already a server with connected clients:

```
// Pseudo-code from scanning logic
if (remoteAdvertisedValue > localAdvertisedValue) {
  if (this.isServer && remoteAdvertisedValue >= 126) {
    this.module.broadcastPacket(`m:d|${remoteAdvertisedValue}`);
    this.isMigratingConnection = true;
    this.connectedClients = [];
  }
}
```

**Migration Flow**:

```mermaid
sequenceDiagram
    participant C as Client
    participant OldS as Old Server
    participant NewS as New Server

    Note over OldS: Discovers NewS with higher advertised value
    OldS->>C: m:d|231 (migrate to value 231)
    Note over C: Client receives migration packet
    C->>C: Disconnect from OldS (no update sound)
    Note over C: state = "migrating"
    C->>C: Begin scanning for new server
    C->>NewS: Connects as client
    NewS->>C: c:1:231 (connection handshake)
    C->>NewS: s:r|1 (session request)
    NewS->>C: s:a|{sessionId} (session established)
    Note over C: state = "synced"
```

**Edge case -- `m:d|0`**: If the migration target disappears or the migration is aborted, server sends `m:d|0`. Client receives this, sets `isMigratingConnection = false`, clears session, and enters `"scanning"` state instead of `"migrating"`.

**Server-side state cleanup on migration**: When a server triggers migration, it clears:
- `connectedClients`
- `askedDisconnectionDevices`
- `askedToStopAlarmClients`
- `rssiUpdatesFromClients`

---

### Sound Synchronization -- `sound:`

**Purpose**: Server broadcasts sound events to all connected clients so they play sounds in sync.

**Format**: `sound:{action}|{soundId}`

| Field | Values | Meaning |
|---|---|---|
| action | `1` | Play sound |
| action | `0` | Stop sound |
| soundId | `0` | "open" sound |
| soundId | `1` | "pair" sound |
| soundId | `2` | "unpair" sound |
| soundId | `3` | "alarm" sound |
| soundId | `4` | "ping" sound |

**Examples**:

```
sound:1|1      # Play "pair" sound on all clients
sound:0|3      # Stop "alarm" sound on all clients
sound:1|4      # Play "ping" sound on all clients
```

**Behavior**: Only the server broadcasts sound packets. When `playSound()` is called on the server, it sends `sound:1|{id}` to all clients. Clients receive the packet, look up the sound ID via `PacketToSoundsMapper`, and play/stop accordingly.

**Exception**: Alarm sounds (`soundId=3`) are never broadcast as `sound:` packets -- they are included in the `a:f|1|*` alarm fire packets and handled separately.

---

### Liveness Check -- `ping:` / `pong:`

**Purpose**: Keepalive mechanism to verify the connection is alive and to sync state after reconnection.

**Format**: `ping:{sessionId}` / `pong:{sessionId}`

| Packet | Direction | Meaning |
|---|---|---|
| `ping:{sessionId}` | Client to Server | **Ping** -- Client verifying connection |
| `pong:{sessionId}` | Server to Client | **Pong** -- Server confirms connection alive, includes session ID |

**Flow**:

```mermaid
sequenceDiagram
    participant C as Client
    participant S as Server

    Note over C: Client receives pong from server
    C->>C: Set state to "synced" if was "connecting"/"scanning"
    C->>C: Clear isAmIClientAllowedToDisconnectEdgeCase
    C->>S: discq:{sessionId} (queue disconnect ack)
    Note over C: Wait 1800ms
    C->>S: ping:{sessionId}
    S->>C: pong:{sessionId}
```

**Edge case**: The `pong` handler on the client also includes a 1800ms delayed `ping` after the initial `discq` acknowledgment. This creates a round-trip verification cycle. If the session ID is undefined when `pong` is received, the client extracts it from the `pong` packet content.

---

### Disconnect Queue Acknowledgment -- `discq:` / `disca:`

**Purpose**: Simple request-acknowledgment pair used to confirm the server received a client's state change before proceeding.

**Format**: `discq:{sessionId}` / `disca:1`

| Packet | Direction | Meaning |
|---|---|---|
| `discq:{sessionId}` | Client to Server | **Request** -- Client acknowledges a state change |
| `disca:1` | Server to Client | **Answer** -- Server confirms receipt |

**Flow**:

```mermaid
sequenceDiagram
    participant C as Client
    participant S as Server

    C->>S: discq:a3f2b8c1
    Note over S: Server receives, sends answer
    S->>C: disca:1
    Note over C: Sets isAmIClientAllowedToDisconnectEdgeCase = true
    Note over C: After 600ms, sends another discq
    Note over C: Clears pingPongTimeout
```

**Usage contexts**:
1. After receiving `a:f|1|u` (alarm fire with user trigger): Client sends `discq` and sets `isAmIClientAllowedToDisconnectEdgeCase = true`
2. After receiving `pong`: Client sends `discq` as part of reconnection handshake
3. During alarm stop (`onStopAlarmPressFinish`): Client sends `discq` before `a:r|f`

---

### Notification Packets -- `n:`

**Purpose**: Proximity sensor notifications (designed but currently commented out in production).

**Format**: `n:{type}|{value}`

| Packet | Status | Meaning |
|---|---|---|
| `n:rssi\|1` | Commented out | **Enable RSSI proximity sensors** on server |
| `n:rss1\|1` | Commented out | **Enable RSSI proximity sensors** (alternate format, also commented out) |

**Note**: These packets were part of the planned RSSI-based proximity alarm system. The server would broadcast `n:rssi|1` to all clients after 3.5 seconds of sync, enabling proximity monitoring. This was disabled for production but the infrastructure remains in the code.

---

## Server-Side Packet Handler -- `handlePacketFromClient`

The server receives all client packets via the `onPacketFromClient` event. The handler returns `true` to broadcast the packet to all clients, or `false` to handle it privately (unicast or no relay).

### Handler Return Values by Packet Type

| Packet | Return | Broadcast? | Behavior |
|---|---|---|---|
| `c:*` | `false` | No | Echoes packet back to sender only |
| `s:*` | `false` | No | Sends `s:a\|{sessionId}` to sender only |
| `d:*` | `false` | No | Manages disconnection state per-client |
| `discq:*` | `false` | No | Sends `disca:1` to sender only |
| `a:*` | `false` | No | Manages alarm state per-client |
| `ping:*` | `false` | No | Sends `pong:{sessionId}` to sender only |

**Important**: The server NEVER broadcasts client-originating packets. All relay behavior is explicit -- the server constructs new packets and sends/broadcasts them itself. The `broadcastPacket` call at the end of `onPacketFromClient` is only reached if `handlePacketFromClient` returns `true`, which it currently never does for any standard packet type.

### Detailed Server Handler Logic

#### `c:` -- Connection Handshake
- Logs the received connection packet
- Echoes the exact packet back to the sender (`sendPacketToClient`)
- Resets UI positions (ball and alarm button to center)
- Returns `false` (no broadcast)

#### `s:` -- Session Request
- Extracts `fireOnDisconnect` from the request (non-zero = true)
- If any client wants fire, sets `hasAnyoneRequestedToFireWhenDisconnect = true`
- Sends `s:a|{sessionId}` to the requesting client
- If `connectedClients.length >= 1`, transitions server to `"synced"` state
- Starts proximity sensor timeout (3.5s delay)
- Returns `false` (no broadcast)

#### `d:` -- Disconnection Request

Handles 4 sub-types:

| Sub-type | Action |
|---|---|
| `d:r\|ff` | Broadcasts `d:a\|a1` to ALL clients (force disconnect) |
| `d:r\|s` | Adds client to `askedDisconnectionDevices` list |
| `d:r\|f` | Removes client from `askedDisconnectionDevices`. If fireOnDisconnect applies, broadcasts `a:f\|1\|a` (alarm) |
| `d:r\|1` | If client is in `askedDisconnectionDevices`: adds to `allowedClientsToDisconnect`, sends `d:a\|a1` (broadcast if all asked, else unicast). If NOT in list: triggers alarm with `a:f\|1\|a` |

#### `discq:` -- Disconnect Queue
- Sends `disca:1` to the requesting client
- Returns `false`

#### `a:` -- Alarm Request

Handles multiple sub-types:

| Sub-type | Action |
|---|---|
| `a:r\|s` | Adds client to `askedToStopAlarmClients`. If ALL clients have asked AND all are in `askedDisconnectionDevices`, sends `d:a\|a` to each (emergency disconnect during alarm) |
| `a:r\|f` | Removes client from `askedToStopAlarmClients` |
| `a:u\|i` | Client went to background. Broadcasts `a:f\|1\|u` to all, sets alarm state |
| `a:u\|rssi\|{value}\|{sense}` | RSSI proximity data from client (see RSSI section) |

#### `ping:` -- Liveness Check
- If server state is `"connecting"` or `"scanning"`, transitions to `"synced"`
- Sends `pong:{sessionId}` to the requesting client
- Returns `false` (no broadcast)

---

## Client-Side Packet Handler -- `handlePacketFromServer`

The client receives all server packets via the `monitorCharacteristicForService` callback on the indicate characteristic. Packets are Base64-decoded via `atob`.

### Detailed Client Handler Logic

#### `c:` -- Connection Echo
- If the received packet matches `connectionPacketSent` (the original handshake):
  - Logs "roundtrip"
  - Sets `isConnecting = false`, `isServer = false`
  - Transitions to `"connected"` state
  - Sends `s:r|{fireOnDisconnect}` with the local preference
- Clears scanning timeout regardless

#### `s:` -- Session Assignment
- Extracts session ID from `s:a|{sessionId}`
- Stores as `this.sessionId`
- Transitions to `"synced"` state
- Resets UI positions (alarm button and ball to center)
- Starts client-side RSSI timeout

#### `d:` -- Disconnection Response

Handles `d:a|*` sub-types:

| Sub-type | Action |
|---|---|
| `d:a\|a` | Server accepted. Clears `pingPongTimeout`, sends `d:r\|1` (confirm) back to server |
| `d:a\|r` | Server rejected. Clears `pingPongTimeout`. No action (client stays connected) |
| `d:a\|a1` | Final accept. Sets `hasReceivedDisconnectFromServer = true`, clears session, disconnects from server |

#### `m:` -- Migration

Handles two cases:

| Sub-type | Action |
|---|---|
| `m:d\|0` | Migration aborted. Sets `hasReceivedDisconnectFromServer = true`, `isMigratingConnection = false` |
| `m:d\|{value}` | Migration active. Sets `isMigratingConnection = true`, disconnects from server (no update sound), clears session, transitions to `"migrating"` or `"scanning"` state |

#### `a:` -- Alarm Fire/Stop

Handles `a:f|*` sub-types:

| Sub-type | Action |
|---|---|
| `a:f\|1\|u` | Alarm fired (user trigger). Sends `discq:{sessionId}`, sets `isAmIClientAllowedToDisconnectEdgeCase = true`, transitions to `"alarm"` |
| `a:f\|1\|a` | Alarm fired (disconnect trigger). Transitions to `"alarm"` state |
| `a:f\|0\|a` | Alarm stopped. Sets `hasReceivedDisconnectFromServer = true`, transitions to `"disconnected"`, stops alarm sound, shows connection error |

#### `sound:` -- Sound Sync
- Extracts action (`0` = stop, `1` = play) and sound ID
- Maps sound ID to sound name via `PacketToSoundsMapper`
- Plays or stops the sound accordingly

#### `n:` -- Notification
- Currently a no-op (commented out logic would show proximity sensor enabled snackbar)

#### `pong:` -- Liveness Response
- If state is `"connecting"` or `"scanning"`, transitions to `"synced"`
- Clears `isAmIClientAllowedToDisconnectEdgeCase`
- Sends `discq:{sessionId}`
- After 1800ms delay, sends `ping:{sessionId}` back to server

#### `disca:` -- Disconnect Queue Ack
- Clears `isAmIClientAllowedToDisconnectEdgeCase`
- After 600ms delay, sends `discq:{sessionId}` and sets `isAmIClientAllowedToDisconnectEdgeCase = true`
- Clears `pingPongTimeout`

---

## RSSI Proximity System

The protocol includes a designed but currently disabled proximity-based alarm system that would trigger alarms when devices drift apart.

### Architecture

```
Client reads RSSI every ~15ms
    |
    v
a:u|rssi|{rssiValue}|{senseValue}  -->  Server
    |
    v
Server maintains sliding window of last 5 RSSI readings per client
    |
    v
Server computes mean per client
    |
    v
If mean >= threshold (lowest sensitivity across all participants)
    |
    v
Alarm fires: a:f|1 (broadcast)
```

### RSSI Packet Format

**Client to Server**: `a:u|rssi|{rssi}|{sense}`

| Field | Type | Meaning |
|---|---|---|
| rssi | integer (negative) | Raw RSSI value from BLE read |
| sense | integer or `null` | User's configured sensitivity threshold |

**Example**:

```
a:u|rssi|-65|60    # RSSI -65dBm, sensitivity threshold 60
a:u|rssi|-42|null  # RSSI -42dBm, no sensitivity configured
```

### Server-Side Processing

1. **Sliding window**: Each client's last 5 RSSI readings are stored in `rssiUpdatesFromClients` map
2. **Mean calculation**: Server computes mean of absolute RSSI values per client
3. **Threshold comparison**: The lowest sensitivity across all participants is used (`hasAnyoneHigherSensibility`)
4. **Alarm trigger**: If any client's mean exceeds the threshold, alarm fires
5. **Auto-disconnect**: A 1-second timeout after alarm fire would force disconnect (currently commented out)

### Sensitivity Negotiation

When a client sends `a:u|rssi|{value}|{sense}`:
- If `sense` is not `null`, the server compares it with its own sensitivity (`StorageKeys.rssiSense`)
- If the client's sense is lower (more sensitive), it becomes the new `hasAnyoneHigherSensibility`
- This means the MOST sensitive participant's threshold wins -- the system becomes as sensitive as the least distant threshold

### Current Status

**All RSSI code is commented out in production.** The infrastructure exists:
- `configureClientRSSITimeout()` -- Client reads RSSI every 15ms (loop disabled)
- `configureServerRSSITimeout()` -- Server processes RSSI data (loop disabled)
- `handleRssiUpdateFromClient()` -- Stores RSSI in sliding window (function exists but callers are commented out)
- `n:rssi|1` / `n:rss1|1` -- Notification packets to enable sensors (not sent)

The system was designed for ~45fps client read rate and ~30fps server processing rate. The 3.5-second delay before enabling proximity sensors after sync prevents false positives during connection establishment.

---

## App State Machine

The protocol drives the following UI states:

```mermaid
stateDiagram-v2
    [*] --> disconnected
    disconnected --> scanning : startScanning()
    disconnected --> synced : became server with client
    scanning --> connecting : found server
    scanning --> synced : became server (no higher value found)
    connecting --> connected : c: handshake roundtrip
    connected --> synced : s:a sessionId received
    synced --> alarm : a:f|1|* received
    synced --> migrating : m:d|{value} received
    synced --> disconnected : d:a|a1 received
    alarm --> disconnected : a:f|0|a received
    migrating --> scanning : disconnected, searching for new server
    migrating --> synced : connected to new server
```

### State Descriptions

| State | Meaning |
|---|---|
| `disconnected` | No active BLE connections. Idle state. |
| `scanning` | Actively scanning for nearby servers. |
| `connecting` | Found a server, establishing BLE connection. |
| `connected` | BLE connected, handshake complete, waiting for session. |
| `synced` | Fully operational. Session active, alarm monitoring enabled. |
| `alarm` | Alarm is firing. Sound looping. Waiting for all parties to agree to stop. |
| `migrating` | Disconnected from old server, scanning for migration target. |

---

## Advertising Value Ranges

The advertised BLE value encodes platform and server priority:

| Range | Platform | Role |
|---|---|---|
| 1-124 | iOS | Client candidate (lower value = longer serving) |
| 125 | iOS | Becoming server |
| 126-249 | Android | Client candidate |
| 251 | Android | Becoming server |

**Server election logic**: During scanning, if a device finds another device with a higher advertised value, it becomes a client. If it finds a lower value, it becomes the server. This creates an implicit priority queue where:
- Android devices always become servers over iOS devices (126+ vs 1-124)
- Among same-platform devices, the one that has been advertising longer (higher value in the current scheme) wins

**Exception**: iOS scanning retries up to 10 times for devices with value <= 125 before accepting, to avoid connecting to another iOS client that hasn't become a server yet.

---

## UUID Sound Mapping

| Packet ID | Sound Name | Audio File | Description |
|---|---|---|---|
| `0` | open | BillyBoy_Camdom_02_Open.mp3 | App open/startup |
| `1` | pair | BillyBoy_Camdom_02_Pair.mp3 | Successful connection |
| `2` | unpair | BillyBoy_Camdom_02_Unpair.mp3 | Disconnection |
| `3` | alarm | BillyBoy_Camdom_Alarm_2.mp3 | Security alarm (loops) |
| `4` | ping | ping.mp3 | Proximity ping |

---

## Connection Lifecycle Summary

```mermaid
sequenceDiagram
    participant U as User
    participant C as Client Device
    participant S as Server Device

    Note over S: Advertising (125/251)
    Note over C: Scanning

    C->>S: BLE Connect
    C->>S: c:0:87 (connection handshake)
    S->>C: c:0:87 (echo)
    Note over C: state = "connected"

    C->>S: s:r|1 (session request, fire=1)
    S->>C: s:a|a3f2b8c1 (session ID)
    Note over C: state = "synced"

    Note over C,S: Active session -- alarm monitoring, proximity sensors

    U->>C: Hold disconnect button
    C->>S: d:r|s (start disconnect)
    Note over S: Client in askedDisconnectionDevices

    U->>S: Hold disconnect button
    Note over S: Server also wants disconnect

    U->>C: Release disconnect
    C->>S: d:r|f (finish disconnect)

    Note over S: All clients asked, server pressed
    S->>C: d:a|a (accepted)
    C->>S: d:r|1 (confirm)
    S->>C: d:a|a1 (final -- all disconnect)

    Note over C: state = "disconnected"
    Note over S: state = "disconnected"
    Note over S: Stops advertising
```

---

## Edge Cases and Defensive Behaviors

### 1. Cross-Client Echo Prevention
Server echoes `c:` packets only to the sender (`sendPacketToClient`), never broadcasts. This prevents Client A from receiving Client B's handshake echo.

### 2. Duplicate Connection Guard
`onDeviceConnected` checks `connectedClients.includes(e.value)` before adding. If a device reconnects rapidly, it is not added twice.

### 3. Orphaned State Cleanup
BLE state changes (`PoweredOff`, `Unauthorized`, etc.) trigger a full cleanup: clear all arrays, maps, timeouts, and reset UI state.

### 4. Background Alarm Trigger
Both `AppState.addEventListener("change")` and Android-specific `AppState.addEventListener("blur")` trigger alarm if the app goes to background during a synced session. Server broadcasts `a:f|1|u`, client sends `a:u|i`.

### 5. Scanning Timeout Recovery
If scanning finds nothing for 7.5 seconds and the device is not connecting/synced, it resets: disconnects, stops scanning, stops advertising, restarts scanning. iOS has an extended retry count (`scanCountRetriesIos`) for slow discovery.

### 6. Server Kill Timeout
When a client disconnects, `shouldKillServerTimeout` is set. If the client reconnects before the timeout, the kill is cancelled (`clearTimeout`).

### 7. Migration Race Condition
If a server discovers a higher-value device while already serving clients, it broadcasts `m:d|{value}` and clears all client state. Clients receive the migration, disconnect, and rescan.

### 8. `isAmIClientAllowedToDisconnectEdgeCase`
A special flag that allows a client to bypass the normal disconnect flow during alarm states. When `true`, the client can send `d:r|ff` or `a:r|ff` to force disconnection. Set to `true` after receiving `a:f|1|u` or `disca:1`. Cleared on `pong:` receipt.

### 9. MTU Negotiation
Client requests 53-byte MTU after connection (`requestMTU(53)`). This limits the maximum Base64-encoded packet size. The protocol is designed to stay well within this limit.

### 10. Audio Mode Override
`playSound()` forces volume to 100% and sets `DoNotMix` interruption mode on both platforms. This ensures alarm sounds are always audible even if the user has the phone on silent or low volume.

---

*Source: `modules/ble-manager/src/BleManager.ts` (CAMDOM condom-app repository)*
