---
name: sprite-mobile-ble
type: analysis
title: "Sprite Mobile — BLE Integration"
description: "Analysis of the BLE layer in Sprite Mobile: device scanning, RSSI monitoring, distance calculation, and comparison with CAMDOM's custom mesh protocol"
tags: [ble, bluetooth, rssi, proximity, kalman-filter, distance-estimation, react-native-ble-plx]
timestamp: 2026-07-21
---

# Sprite Mobile — BLE Integration

## Overview

Sprite Mobile uses Bluetooth Low Energy (BLE) for a single purpose: monitoring the RSSI signal strength of a paired Knocklace jewelry device to estimate physical distance. Unlike [[camdom]], which implements a custom BLE mesh networking protocol for multi-device coordination, Sprite uses a standard BLE peripheral scanning model — one phone monitors one device at a time.

## BLE Library: Forked react-native-ble-plx

The project vendors `react-native-ble-plx` as a local module at `modules/react-native-ble-plx/`. This is a complete fork of the library including all native source code (Java for Android, Swift/ObjC for iOS).

### Why Fork?

The fork allows direct modification of:
- **Scan parameters** — `allowDuplicates: true`, `legacyScan: true`, `ScanMode.LowLatency`
- **State restoration** — `restoreStateIdentifier: 'com.knocklace.ble'` for background BLE
- **Event handling** — Custom error codes (101 = unauthorized, 102 = powered off) are re-emitted as state change events
- **Native behavior** — Platform-specific scan filtering and callback types

### Scan Configuration

```typescript
bleManager.startDeviceScan(null, {
  allowDuplicates: true,     // Get repeated RSSI updates from same device
  legacyScan: true,          // Use legacy BLE scan (wider compatibility)
  scanMode: ScanMode.LowLatency,  // Max scan frequency
  callbackType: ScanCallbackType.AllMatches,  // (RSSI monitoring) or FirstMatch (discovery)
}, callback);
```

Two scan modes are used:
1. **Discovery scan** (`ScanCallbackType.FirstMatch`) — Finds new devices, stops when paired device is found
2. **RSSI monitoring scan** (`ScanCallbackType.AllMatches`) — Continuous RSSI updates from paired device only

## Device Discovery Flow

```mermaid
sequenceDiagram
    participant App as ApplicationService
    participant BLE as BleManager
    participant Remote as Remote Config
    participant Redux as Redux Store

    App->>Remote: getRemoteConfig()
    Remote-->>App: customConfigPerDevice[]
    App->>App: updateKnownTags(knownTags)
    App->>BLE: startDeviceScan(FirstMatch)
    loop Each discovered device
        BLE-->>App: (error, device)
        App->>App: isKnownDevice(device.name)
        alt Device is known
            App->>App: find customConfig by knownAs
            App->>App: setConnectedDevice(device)
            App->>BLE: stopDeviceScan()
            App->>Redux: dispatch(setPairedDevice)
            App->>App: AsyncStorage.setItem('pairedDeviceId')
            App->>App: monitorRssiForPairedDevice()
        end
    end
```

### Device Identification

Devices are identified differently on each platform:

| Platform | Identification Field | Example |
|---|---|---|
| iOS | `device.name` or `device.localName` | `"sprite6"`, `"SPRITE8"` |
| Android | `device.id` (MAC address) | `"E6:4C:24:54:AC:45"` |

The `DeviceCompanionClass` singleton maintains a list of known device names (from remote config `knownTags`) and checks scanned devices against this list. It normalizes names to lowercase for comparison.

### Remote Config Device Profiles

Each known device has a calibration profile:

```typescript
interface DeviceCustomConfig {
  customConfigEnabled: boolean;
  customName: string;       // "OVAL 2", "CIRCLE", "SQUARE", etc.
  jewelModel: number;       // 0=circle, 1=square, 2=oval
  knownAs: string;          // BLE name (iOS) or MAC (Android)
  rssiAt1Meter: number;     // Calibration: expected RSSI at 1m (-60 to -90)
  rssiPow?: number;         // Path loss exponent (0.8 to 1.0)
}
```

Example devices from `remoteConfig.json`:

| customName | knownAs (iOS) | rssiAt1Meter | rssiPow | jewelModel |
|---|---|---|---|---|
| OVAL 2 | sprite6 | -80 | 0.8 | 2 |
| CIRCLE | SPRITE8 | -75 | 1.0 | 3 |
| OVAL | SPRITE9 | -85 | 1.0 | 2 |
| SQUARE | SPRITE10 | -90 | 1.0 | 1 |
| PAY4BRAIN | PAY4BRAIN | -70 | 1.0 | 2 |

## RSSI Monitoring

### Continuous Scan

Once paired, `monitorRssiForPairedDevice()` starts a continuous BLE scan filtered to the paired device's ID:

```typescript
this.bleManager?.startDeviceScan(
  null,
  {
    allowDuplicates: true,
    legacyScan: true,
    scanMode: ScanMode.LowLatency,
    callbackType: ScanCallbackType.AllMatches,
  },
  (error, device) => {
    if (device?.id === this.pairedDeviceId && device.rssi) {
      this.emit(ApplicationEvent.OnRssiUpdate, device.rssi);
    }
  },
);
```

### RSSI-to-Distance Conversion

The distance calculation uses a log-distance path loss model:

```typescript
calculateDistance(rssi: number): number {
  const txPower = this.deviceReference?.rssiAt1Meter ?? this.factor;  // default: -79
  const ratio = txPower - rssi;
  const ratiof = Math.pow(10, (ratio * (this.deviceReference?.rssiPow ?? 1)) / 10);
  const rawVal = Math.sqrt(ratiof);
  return AppModule.filter(Math.ceil(rawVal), this.kalmanVariation);
}
```

**Formula**: `distance = sqrt(10^((txPower - rssi) * pathLossExponent / 10))`

Where:
- `txPower` = RSSI at 1 meter (calibrated per device)
- `rssi` = current RSSI reading
- `pathLossExponent` = environment-specific constant (0.8-1.0)
- `kalmanVariation` = rolling average window size (3 on iOS, 7 on Android)

### Native Kalman Filter

The raw distance value is passed to the native module for smoothing:

**iOS** (`AppModule.swift`):
```swift
func filterReading(calculatedDist: Double, variation: Double) -> NSNumber {
    // Clamp to [0, 100]
    // Handle NaN (use last valid reading)
    // Maintain rolling window of 20 readings
    // Deduplicate
    // Take last N readings (N = variation)
    // Return mean
}
```

**Android** (`AppModuleModule.kt`):
```kotlin
override fun filter(rssi: Double, variation: Double): Double {
    // Clamp to [0, 100]
    // Handle NaN (use last valid reading)
    // Maintain rolling window of 10-20 readings
    // Deduplicate via distinct()
    // Take last N readings (N = variation)
    // Return average
}
```

### Distance Update Pipeline

```mermaid
graph LR
    RSSI["Raw RSSI"] --> CALC["calculateDistance()"]
    CALC --> NATIVE["Native filter()"]
    NATIVE --> DEBOUNCE["Debounce (timeoutSecs)"]
    DEBOUNCE --> REDUX["Redux: updatePairedDeviceApproximatedDistance"]
    DEBOUNCE --> ONDIST["OnDistanceUpdate()"]
    ONDIST --> THRESHOLD{"distance <= emergencyRange?"}
    THRESHOLD -->|Yes| ALERT["fireYoutube()"]
    THRESHOLD -->|No| RANGES["Check rangesConfig"]
    RANGES --> NOTIF["showNotification()"]
```

### Debounce Mechanism

A timeout (`timeoutSecs`) resets the distance to -1 if no RSSI updates are received:

```typescript
this.debounceTimeoutId = setTimeout(() => {
  AppModule.resetPositionsArray();
  store.dispatch(Actions.updatePairedDeviceApproximatedDistance(-1));
}, this.timeoutSecs);
```

Default timeouts: 10s (iOS), 3s (Android). This handles cases where the device goes out of range or BLE connection is lost.

### Notification Suppression

The system uses `lastTreeDistances` (last 3 readings) to suppress jitter:

```typescript
let canDisplay = true;
this.lastTreeDistances.forEach(d => {
  canDisplay = !(Math.abs(distance - d) > 2 || Math.abs(d - distance) > 2);
});
```

A notification only fires if the current distance is within 2m of the last 3 readings. This prevents false alerts from RSSI spikes.

## Background BLE Scanning

### Android

`RNBackgroundActionsTask` extends `HeadlessJsTaskService`:
- Creates a foreground service with persistent notification ("Alerta Sprite — Knocklace Collection is running in background")
- Uses `FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE` on API 33+
- Notification channel: `RN_BACKGROUND_ACTIONS_CHANNEL` with `IMPORTANCE_LOW`
- Starts via `reactApplicationContext.startService(intent)`

### iOS

`BGProcessingTask` registered with identifier `com.knocklace.BgTask.ScanningForDevices`:
- Currently a placeholder implementation
- The `BleManager` is initialized with `restoreStateIdentifier: 'com.knocklace.ble'` for state restoration

## Event Flow

```mermaid
graph TB
    subgraph "BLE Layer"
        BM["BleManager"] --> STATE["onStateChange"]
        BM --> SCAN["startDeviceScan callback"]
    end

    subgraph "ApplicationService"
        STATE --> EMIT_STATE["emit(OnBleStateChange)"]
        SCAN --> EMIT_RSSI["emit(OnRssiUpdate)"]
        EMIT_RSSI --> CALC["calculateDistance()"]
        CALC --> EMIT_DIST["emit(OnDistanceUpdate)"]
    end

    subgraph "Event Handlers"
        EMIT_STATE --> H_STATE["Handle PoweredOn/Off/Unknown"]
        EMIT_DIST --> H_DIST["Handle distance update"]
        H_DIST --> REDUX1["dispatch(updatePairedDeviceApproximatedDistance)"]
        H_DIST --> H_ALERT["OnDistanceUpdate()"]
    end

    subgraph "Alert System"
        H_ALERT --> FIRE{"Emergency range?"}
        FIRE -->|Yes| YOUTUBE["fireYoutube()"]
        FIRE -->|No| RANGE["Check rangesConfig"]
        RANGE --> NOTIFY["displayNotification()"]
    end
```

## Comparison with CAMDOM

| Aspect | Sprite Mobile | CAMDOM |
|---|---|---|
| **BLE Pattern** | Peripheral scanning (1 phone → 1 device) | Mesh networking (N devices ↔ N devices) |
| **Protocol** | Standard BLE advertising | Custom JSI C++ state machine |
| **Data Exchanged** | RSSI only (no GATT characteristics used) | Bidirectional consent signals |
| **Connection** | No GATT connection — scan-only | Persistent connections with characteristic reads/writes |
| **Multi-device** | Single device at a time | Multiple devices in mesh topology |
| **Distance Model** | Log-distance path loss + Kalman filter | Mesh proximity detection |
| **Background** | Foreground service (Android) / BGProcessingTask (iOS) | Foreground service + QUIC/TCP multiplexing |
| **Native Code** | Swift (Kalman filter) / Kotlin (overlay, BG tasks) | Full JSI C++ bridge (race condition exploitation) |
| **Library** | Forked react-native-ble-plx | Custom BLE implementation |

### Key Difference: Scan-Only vs. Connected

Sprite never establishes a GATT connection to the Knocklace device. It only scans for advertising packets and reads the RSSI value. This is a fundamentally different approach from CAMDOM, which establishes persistent BLE connections and reads/writes GATT characteristics for bidirectional communication.

The scan-only approach means:
- Lower battery consumption
- No pairing required (at OS level)
- No GATT service/characteristic discovery
- RSSI-only data (no payload)
- Simpler error handling (scan failures vs. connection drops)

### Key Difference: Single Device vs. Mesh

CAMDOM's BLE mesh requires:
- Multiple simultaneous connections
- Custom protocol for device discovery and signal relay
- State synchronization across devices
- JSI C++ bridge for low-latency state management

Sprite's single-device model allows:
- Simple Redux state (one `pairedDevice` object)
- No state synchronization
- JavaScript-level event handling is sufficient
- TurboModule for native bridge (adequate for the use case)

## Error Handling

BLE errors are mapped to state changes:

| Error Code | Meaning | Action |
|---|---|---|
| 101 | Unauthorized | Emit `OnBleStateChange` with `State.Unauthorized` → retry permission request after 2s |
| 102 | Powered off | Emit `OnBleStateChange` with `State.PoweredOff` → stop all scans |
| Other | Scan failure | Stop foreground scan, log error |

## State Restoration

The `BleManager` is initialized with `restoreStateIdentifier: 'com.knocklace.ble'`, enabling iOS BLE state restoration. When the app is relaunched after being terminated by the system, the BLE manager can restore its previous state and resume scanning.

On Android, state restoration is handled by the `pairedDeviceId` stored in `AsyncStorage`. On app launch, the service checks for a stored device ID and resumes monitoring if found.

## Performance Considerations

1. **Scan frequency**: `ScanMode.LowLatency` provides the fastest scan interval (~100ms) at the cost of higher battery usage
2. **Kalman filter**: Native implementation avoids JS bridge overhead for the most frequent calculation
3. **Debounce**: Prevents UI jank from rapid RSSI fluctuations
4. **Notification suppression**: Last-3-readings check prevents notification spam
5. **Memoization**: `RadarBaseAndEffect` and `BackgroundVideo` are wrapped in `React.memo`

## Cross-References

- [[sprite-mobile]] — Hub overview
- [[sprite-mobile-architecture]] — Full architecture context
- [[camdom]] — CAMDOM's BLE mesh approach
- [[camdom-architecture]] — CAMDOM's JSI C++ state machine for BLE
