WikifitaGitHub live67e8de5
outro · sprite-mobile/sprite-mobile-knocklace

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

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.

Baixar raw

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:

WeightUsage
TCCC-UnityHeadline-BlackPrimary headings, modal titles
TCCC-UnityHeadline-BoldButton labels, navigation
TCCC-UnityHeadline-MediumSecondary text
TCCC-UnityHeadline-RegularBody text
TCCC-UnityHeadline-LightSubtle labels
TCCC-UnityCondensed-BoldCompact UI elements
TCCC-UnityCondensed-MediumDense 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 IDShapeVisual AssetRemote Config NameDescription
0Circleanimated-jewel-1.gif"CIRCLE"Classic round locket
1Squareanimated-jewel-3.gif"SQUARE"Geometric square locket
2Ovalanimated-jewel-2.gif"OVAL" / "OVAL 2"Elongated oval locket

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

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

The mapping from model ID to asset:

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:

{
  "customConfigPerDevice": [
    {
      "customConfigEnabled": true,
      "customName": "OVAL 2",
      "jewelModel": 2,
      "knownAs": "sprite6",
      "rssiAt1Meter": -80,
      "rssiPow": 0.8
    }
  ]
}
ParameterMeaningDefault
rssiAt1MeterRSSI reading at exactly 1 meter (calibration point)-79 dBm
rssiPowPath loss exponent (environment-dependent)1.0
knownAsBLE identifier (iOS: device name; Android: MAC address)
customNameDisplay name shown to user
jewelModelPhysical 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:

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:

PlatformIdentification MethodReason
iOSdevice.name or device.localNameCoreBluetooth exposes BLE advertisement names reliably
Androiddevice.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:

// 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:

PlatformMechanismImplementation
iOSBGTaskSchedulerTask ID: com.knocklace.BgTask.ScanningForDevices
AndroidHeadlessJsTaskServiceForeground 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:

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;
}
ParameterSymbolSourceTypical Value
rssiAt1MeterRSSI(d0)Remote config per-device-79 to -80 dBm
rssiPownRemote config per-device0.8 to 1.0
Measured RSSIRSSI(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:

FilePurpose
KalmanFilter.swiftCore filter logic
Matrix.swiftMatrix operations using Accelerate.framework
KalmanFilterType.swiftFilter type definitions
DoubleExtension.swiftNumeric 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:

// 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

EventBehavior
RSSI receivedCalculate distance, filter, update Redux
Signal lostAfter timeoutSecs (10s iOS / 3s Android), distance resets to undefined (displays "---")
Device in rangeOnDistanceUpdate fires, checks against emergencyRange
Alert threshold crossedNotification + URL open (once per crossing, tracked by notificationAlreadyShownForRange)

5. User Flow: Pairing to Alarm

5.1 Complete Flow Diagram

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:

ControlActionRedux Action
Alert toggleEnable/disable proximity alertssetAlertState(boolean)
Distance sliderSet threshold (3-7m, step 1)setEmergencyRange(number)
URL inputSet emergency URLsetEmergencyUrl(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:

PlatformFallback Path
iOSmodules/app-module/src/remoteConfig.json
Androidmodules/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:

AspectKnocklace CollectionCAMDOM
PurposeProximity detection + party alarmDigital consent protection
BLE patternOne-to-one (phone to single locket)Many-to-many BLE mesh
Data over BLERSSI only (no GATT)Full state machine via GATT characteristics
Distance methodRSSI path loss model + sliding windowPresence-based (not distance)
Native moduleKalman filter + background scanJSI C++ state machine
Background modeBGTaskScheduler / HeadlessJsTaskServiceBLE peripheral mode
Alert mechanismPush notification + open URLCamera/mic lockout via /dev/null
InnovationCalibrated path loss estimationRace 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)