---
name: sprite-mobile-knocklace
type: reference
title: "Knocklace Collection — Brand Context, BLE Beacon Protocol & User Flow"
description: "Deep-dive into the Sprite/Coca-Cola Knocklace Collection: three physical locket models, RSSI-based distance estimation, BLE beacon protocol, and the full user flow from pairing to alarm."
tags: [sprite, knocklace, ble, proximity, wearable, coca-cola, rssi, beacon, react-native]
timestamp: 2026-07-22
---

# Knocklace Collection — Brand Context, BLE Beacon Protocol & User Flow

> **Source:** Reverse-engineering of `/Users/alefita/workdir/sprite-mobile/`
> **Version:** 4.24.0 (JS) / 4.24.1 (native module)

---

## 1. Brand Context

### 1.1 Sprite and Coca-Cola

**Knocklace Collection** is a BLE wearable product line under the **Sprite** brand, owned by The Coca-Cola Company (TCCC). Sprite's global marketing identity centers on refreshment, youth culture, and irreverent humor — the brand's green (#40c965) is as iconic as Coca-Cola's red.

The app uses Coca-Cola's proprietary **TCCC Unity** typeface family across seven weights:

| Weight | Usage |
|--------|-------|
| `TCCC-UnityHeadline-Black` | Primary headings, modal titles |
| `TCCC-UnityHeadline-Bold` | Button labels, navigation |
| `TCCC-UnityHeadline-Medium` | Secondary text |
| `TCCC-UnityHeadline-Regular` | Body text |
| `TCCC-UnityHeadline-Light` | Subtle labels |
| `TCCC-UnityCondensed-Bold` | Compact UI elements |
| `TCCC-UnityCondensed-Medium` | Dense information areas |

All UI text renders in **uppercase**, consistent with Sprite's bold visual identity. The color palette follows strict Sprite brand guidelines:

- **Primary green:** `#40c965`
- **Dark green:** `#0d2814`, `#04240D`, `#133c1e`
- **Background:** `#000` (black)
- **Text:** `#fff` (white)

### 1.2 The "Knocklace" Name

The name is a portmanteau: **knock** (proximity detection / knocking) + **necklace** (the wearable form factor) + **collection** (the product line). The concept is literal: when someone wearing a Knocklace "knocks" into your proximity radius, you get alerted.

### 1.3 The Social Use Case

The app functions as a **proximity privacy alarm** with tongue-in-cheek marketing. The notification copy reveals the intent:

> **EN:** "Cease any potentially embarrassing activities immediately"
> **PT:** "Interromper imediatamente quaisquer atividades constrangedoras"

This is a **party/social privacy tool**. When someone wearing the Knocklace approaches your phone within the configured distance, the app warns you to close whatever you are looking at. The emergency action — opening a YouTube URL — provides plausible denuity ("I was just watching a video").

---

## 2. The Three Locket Models

### 2.1 Physical Variants

The remote config and codebase reveal three distinct physical locket models, each containing a BLE beacon:

| Model ID | Shape | Visual Asset | Remote Config Name | Description |
|----------|-------|-------------|-------------------|-------------|
| `0` | Circle | `animated-jewel-1.gif` | `"CIRCLE"` | Classic round locket |
| `1` | Square | `animated-jewel-3.gif` | `"SQUARE"` | Geometric square locket |
| `2` | Oval | `animated-jewel-2.gif` | `"OVAL"` / `"OVAL 2"` | Elongated oval locket |

Each model has its own animated GIF asset rendered by the `AnimatedLocket` component:

```typescript
interface AnimatedJewelComponentProps {
  model?: 'circle' | 'square' | 'oval' | number;  // 0=circle, 1=square, 2=oval
}
```

The mapping from model ID to asset:

```typescript
const ASSET_MAP = {
  0: require('./assets/animated-jewel-1.gif'),  // circle
  1: require('./assets/animated-jewel-3.gif'),  // square
  2: require('./assets/animated-jewel-2.gif'),  // oval
};
```

### 2.2 Per-Device Calibration

Each physical locket broadcasts at slightly different power levels. The remote config compensates with per-device calibration:

```json
{
  "customConfigPerDevice": [
    {
      "customConfigEnabled": true,
      "customName": "OVAL 2",
      "jewelModel": 2,
      "knownAs": "sprite6",
      "rssiAt1Meter": -80,
      "rssiPow": 0.8
    }
  ]
}
```

| Parameter | Meaning | Default |
|-----------|---------|---------|
| `rssiAt1Meter` | RSSI reading at exactly 1 meter (calibration point) | `-79` dBm |
| `rssiPow` | Path loss exponent (environment-dependent) | `1.0` |
| `knownAs` | BLE identifier (iOS: device name; Android: MAC address) | — |
| `customName` | Display name shown to user | — |
| `jewelModel` | Physical shape (0/1/2) | — |

### 2.3 Development/Test Devices

The bundled fallback configs contain development device names that reveal the naming convention:

- `SPRITE8`, `SPRITE9`, `SPRITE10` — numbered Sprite test devices
- `lucas-iot`, `maya-iot` — developer-named IoT prototypes
- `PAY4BRAIN` — unknown, possibly a marketing stunt device

---

## 3. BLE Beacon Protocol

### 3.1 Scanning Configuration

The app uses a locally forked `react-native-ble-plx` (v3.1.2, from `dotintent`) with aggressive scanning parameters:

```typescript
BleManager.startDeviceScan(null, {
  allowDuplicates: true,      // receive repeated advertisements
  legacyScan: true,            // Android legacy scan API
  scanMode: ScanMode.LowLatency,           // max scan frequency
  callbackType: ScanCallbackType.AllMatches // report every match
});
```

`allowDuplicates: true` is critical — it means the BLE stack reports every advertisement from every device, not just the first discovery. This enables continuous RSSI polling without establishing a GATT connection.

### 3.2 Device Identification

The identification strategy differs by platform:

| Platform | Identification Method | Reason |
|----------|----------------------|--------|
| **iOS** | `device.name` or `device.localName` | CoreBluetooth exposes BLE advertisement names reliably |
| **Android** | `device.id` (MAC address) | Android BLE does not reliably expose device names in scan results |

The `DeviceCompanionClass` singleton checks each scanned device against `knownTags` from the remote config:

```typescript
// Pseudocode
isKnownDevice(device): boolean {
  if (Platform.OS === 'ios') {
    return knownTags.includes(device.name) || knownTags.includes(device.localName);
  } else {
    return knownTags.includes(device.id);
  }
}
```

### 3.3 No GATT Connection

A key architectural distinction: the Knocklace app **never establishes a GATT connection** to the locket. It reads RSSI values purely from advertisement broadcasts. This means:

- No service discovery
- No characteristic read/write
- No pairing/bonding at the BLE level
- Lower power consumption on both sides
- The locket is a simple beacon, not a connected peripheral

### 3.4 Background Scanning

Background BLE scanning is handled natively on each platform:

| Platform | Mechanism | Implementation |
|----------|-----------|----------------|
| **iOS** | `BGTaskScheduler` | Task ID: `com.knocklace.BgTask.ScanningForDevices` |
| **Android** | `HeadlessJsTaskService` | Foreground service with persistent notification: "Alerta Sprite / Knocklace Collection is running in background" |

The `BleManager` is initialized with `restoreStateIdentifier: 'com.knocklace.ble'` to survive app termination on iOS.

---

## 4. RSSI-Based Distance Estimation

### 4.1 The Path Loss Model

Distance is estimated from RSSI using the **log-distance path loss model**, a standard radio propagation model:

```
RSSI(d) = RSSI(d0) - 10 * n * log10(d / d0)
```

Solving for distance `d` given RSSI measurement and reference RSSI at `d0 = 1m`:

```typescript
function calculateDistance(rssi: number, device: DeviceReference): number {
  const txPower = device.rssiAt1Meter ?? -79;       // RSSI at 1 meter
  const ratio = txPower - rssi;                       // signal attenuation
  const ratiof = Math.pow(10, (ratio * (device.rssiPow ?? 1)) / 10);  // path loss
  const rawVal = Math.sqrt(ratiof);                   // distance in meters
  return rawVal;
}
```

| Parameter | Symbol | Source | Typical Value |
|-----------|--------|--------|---------------|
| `rssiAt1Meter` | RSSI(d0) | Remote config per-device | -79 to -80 dBm |
| `rssiPow` | n | Remote config per-device | 0.8 to 1.0 |
| Measured RSSI | RSSI(d) | BLE scan callback | -40 to -90 dBm |

The path loss exponent `n` controls how quickly signal attenuates:
- `n = 1.0`: Free-space propagation (open field)
- `n = 0.8`: Less attenuation than free-space (unusual, suggests calibration offset)
- `n = 2.0`: Typical indoor environment

### 4.2 Signal Filtering Pipeline

Raw RSSI is noisy. The app applies a multi-stage filtering pipeline:

```
BLE Scan (raw RSSI)
  |
  v
ApplicationService.emit(OnRssiUpdate, rssi)
  |
  v
calculateDistance(rssi) --> raw distance in meters
  |
  v
AppModule.filter(Math.ceil(rawVal), kalmanVariation)
  |
  +-- iOS: Kalman Filter (matrix-based, Accelerate.framework)
  |        + sliding window mean of last N unique values
  |
  +-- Android: Sliding window mean of last N unique values only
  |
  v
Redux dispatch: updatePairedDeviceApproximatedDistance(Math.floor(dist))
  |
  v
OnDistanceUpdate(dist) --> alert logic
```

### 4.3 The Kalman Filter (iOS, Dormant)

The iOS native module contains a full **matrix-based Kalman filter** implementation across four Swift files:

| File | Purpose |
|------|---------|
| `KalmanFilter.swift` | Core filter logic |
| `Matrix.swift` | Matrix operations using Accelerate.framework |
| `KalmanFilterType.swift` | Filter type definitions |
| `DoubleExtension.swift` | Numeric extensions |

The matrix implementation uses Apple's high-performance linear algebra:
- `vDSP` for vectorized operations
- `cblas_dgemm` for matrix multiplication
- `dgetrf_` / `dgetri_` (LAPACK) for matrix inversion

**However**, the actual `filter()` method in `AppModule.swift` uses a simpler **sliding window mean** over the last N unique distance values (where N = `radarSteps` from remote config, typically 3-7). The Kalman filter code is present but **not invoked in the current code path** — it is dormant dead code.

### 4.4 The Active Filter (Both Platforms)

The active filtering algorithm on both platforms is identical:

```swift
// Pseudocode for AppModule.filter()
func filter(rssi: Int, variation: Int) -> Int {
  positionsArray.append(rssi)
  if positionsArray.count > variation {
    positionsArray.removeFirst()
  }
  let uniqueValues = Set(positionsArray)
  let mean = uniqueValues.reduce(0, +) / uniqueValues.count
  return mean
}
```

The `resetPositionsArray()` method clears the sliding window, called when the alert fires or the device disconnects.

### 4.5 Distance Update Lifecycle

| Event | Behavior |
|-------|----------|
| RSSI received | Calculate distance, filter, update Redux |
| Signal lost | After `timeoutSecs` (10s iOS / 3s Android), distance resets to `undefined` (displays `"---"`) |
| Device in range | `OnDistanceUpdate` fires, checks against `emergencyRange` |
| Alert threshold crossed | Notification + URL open (once per crossing, tracked by `notificationAlreadyShownForRange`) |

---

## 5. User Flow: Pairing to Alarm

### 5.1 Complete Flow Diagram

```mermaid
flowchart TD
    A[App Launch] --> B{Android Overlay Permission?}
    B -->|Not Granted| C[OverlayModal]
    C --> C1[Request SYSTEM_ALERT_WINDOW]
    C1 --> C2[Poll every 1s until granted]
    C2 --> D
    B -->|Granted| D{Bluetooth Enabled?}
    D -->|Off| E[BluetoothStateModal]
    E --> E1[User enables Bluetooth]
    E1 --> F
    D -->|On| F{Onboarding Complete?}
    F -->|No| G[OnboardingScreen]
    G --> G1[Start BLE Scan]
    G1 --> G2{Known Device Found?}
    G2 -->|Yes| G3[Show 'Found Device!' pill]
    G3 --> G4[Auto-pair & store in Redux]
    G4 --> G5[Mark onboarding complete]
    G5 --> H
    G2 -->|No| G6[Show spinner, keep scanning]
    G6 --> G2
    F -->|Yes| H[HomeScreen]
    H --> H1[Animated background + 2 buttons]
    H1 --> I[Radar Button]
    H1 --> J[Config Button]
    I --> K[RadarScreen]
    K --> K1[Live distance readout]
    K --> K2[Rotating radar sweep animation]
    K --> K3[Locket image + unpair option]
    J --> L[ConfigScreen]
    L --> L1[Alert On/Off toggle]
    L --> L2[Distance slider 3-7m]
    L --> L3[Emergency URL input]
```

### 5.2 First Run — Onboarding

1. **Permission cascade**: Location (Android) -> Bluetooth (Android) -> Notifications (both), each with 1-second delays between requests
2. **BLE scan starts** with `startDeviceScan(null, { allowDuplicates: true })`
3. **Device matching**: Each scanned advertisement is checked against `knownTags` from remote config
4. **Auto-pair**: When a known device is found, the `SearchingDeviceIndicator` turns green ("Found Device!") and the device is stored in Redux (`setPairedDevice`)
5. **Onboarding complete**: `setOnboardingState(true)` persists to AsyncStorage

### 5.3 Home Screen

The home screen is a visual hub with:
- **Animated background**: Platform-specific (iOS: static PNG; Android: Skia-rendered animated GIF)
- **Radar button**: Animated with random 90-degree pulses every 1.4 seconds (`infinityPulse`)
- **Config button**: Animated with continuous 360-degree rotation (`infinityRotate`)

Both buttons use spring-physics width animation from 0 to 70% of screen width on mount.

### 5.4 Configuration

Three controls on the Config screen:

| Control | Action | Redux Action |
|---------|--------|-------------|
| Alert toggle | Enable/disable proximity alerts | `setAlertState(boolean)` |
| Distance slider | Set threshold (3-7m, step 1) | `setEmergencyRange(number)` |
| URL input | Set emergency URL | `setEmergencyUrl(string)` |

The slider's animated label tracks the thumb position in real-time. Default emergency range: 3 meters. Default emergency URL: from remote config's `defaultVideo` field.

### 5.5 Radar Screen

The radar screen displays:
- **Distance readout**: Live meter reading (e.g., "5m") or "---" when signal is lost
- **Radar visualization**: Static base image with continuously rotating sweep overlay (5-second rotation cycle via `react-native-reanimated`)
- **Device info**: `ConnectionStateManagerView` shows the paired device's custom name in an animated pill, with a locket image. Long-pressing the locket triggers an unpair confirmation dialog

### 5.6 Alert Triggering

When the filtered distance falls below `emergencyRange`:

1. **Deduplication check**: `notificationAlreadyShownForRange` tracks which thresholds have already fired
2. **One-shot guard**: `isAlertAlreadyShown` prevents re-firing the same alert
3. **Actions**:
   - Critical push notification via `@notifee/react-native`
   - Open emergency URL in browser (via `Linking.openURL`)
4. **Reset**: `resetPositionsArray()` clears the filter buffer; `resetAlertShownState()` re-arms for next approach

### 5.7 Remote Config

The app fetches configuration from a Google Cloud Run endpoint (`API_URL` from `.env`). If the fetch fails, it falls back to bundled JSON files:

| Platform | Fallback Path |
|----------|---------------|
| iOS | `modules/app-module/src/remoteConfig.json` |
| Android | `modules/app-module/src/remoteConfigAndroid.json` |

The remote config controls:
- Device identification (`knownTags`, `customConfigPerDevice`)
- BLE calibration (`rssiAt1Meter`, `rssiPow` per device)
- Filter behavior (`radarSteps`, `timeoutSecs`)
- UI features (`enableDot`)
- Emergency defaults (`defaultVideo`)
- Notification thresholds (`rangesConfig`)

---

## 6. Comparison with CAMDOM

Both the Knocklace app and [[camdom]] share a BLE foundation but serve fundamentally different purposes:

| Aspect | Knocklace Collection | CAMDOM |
|--------|---------------------|--------|
| **Purpose** | Proximity detection + party alarm | Digital consent protection |
| **BLE pattern** | One-to-one (phone to single locket) | Many-to-many BLE mesh |
| **Data over BLE** | RSSI only (no GATT) | Full state machine via GATT characteristics |
| **Distance method** | RSSI path loss model + sliding window | Presence-based (not distance) |
| **Native module** | Kalman filter + background scan | JSI C++ state machine |
| **Background mode** | BGTaskScheduler / HeadlessJsTaskService | BLE peripheral mode |
| **Alert mechanism** | Push notification + open URL | Camera/mic lockout via `/dev/null` |
| **Innovation** | Calibrated path loss estimation | Race condition exploitation for QUIC/TCP ACK multiplexing |

The Knocklace app is a straightforward BLE beacon consumer. CAMDOM is a cross-platform BLE mesh networking system with a C++ JSI state machine. The technical complexity is not comparable — but the Knocklace app's calibration system (per-device `rssiAt1Meter` and `rssiPow`) shows thoughtful engineering for real-world BLE noise.

---

## Cross-References

- [[sprite-mobile-redux]] — Redux store architecture that manages pairing state and distance updates
- [[sprite-mobile-code-review]] — Bug analysis including the BLE toggle no-op and permissions reducer issue
- [[camdom]] — Alefita's BLE mesh networking system (Cannes Lions, Epica Grand Prix)
- [[preferencias-tecnicas]] — Alefita's technical preferences (React Native ecosystem)
