WikifitaGitHub live67e8de5
outro · camdom/camdom-architecture

CAMDOM — Technical Architecture

BLE mesh privacy shield: custom native modules, cross-platform interop, connection state machine, advertising system, hardware blocking.

Baixar raw

CAMDOM — Technical Architecture

Overview

CAMDOM (Condom App) is a BLE-based digital consent protection system created by Alef Oliveira (Alefita) for Billy Boy (Innocean Berlin). The app transforms smartphones into a proximity-sensitive privacy shield using Bluetooth Low Energy mesh networking — without requiring internet connectivity, cloud services, or data collection.

The Core Concept

Two phones running CAMDOM form an encrypted BLE link. When one device moves beyond a configurable proximity threshold (measured via RSSI), both devices trigger synchronized alarm sounds. This creates an "invisible tether" — a hardware-level protection mechanism that operates below the OS layer and cannot be bypassed by software manipulation.

Awards

  • Cannes Lions Titanium Shortlist
  • Epica Grand Prix
  • Clio Gold
  • ADCE Gold
  • D&AD
  • The One Show
  • Gerety Gold

Technical Constraints

  • Cross-platform: Android ↔ iOS interop required
  • Offline-first: No internet dependency for core functionality
  • Real-time: Sub-second alarm synchronization via BLE
  • Battery-efficient: Background operation with minimal power consumption
  • Privacy-by-design: Zero data collection, GDPR compliant

System Architecture

High-Level Overview

┌─────────────────────────────────────────────────────────────────┐
│                    React Native Application                     │
│                         (TypeScript)                            │
├─────────────────────────────────────────────────────────────────┤
│                     Expo Modules Layer                          │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │              BleManager (TypeScript)                     │   │
│  │    - State machine management                           │   │
│  │    - Sound synchronization                              │   │
│  │    - Packet protocol handling                           │   │
│  │    - Advertising value election                         │   │
│  └─────────────────────────────────────────────────────────┘   │
├─────────────────────────────────────────────────────────────────┤
│                  Native Module Bridge                           │
│  ┌─────────────────────┐    ┌─────────────────────┐           │
│  │   BleManagerModule  │    │   BleManagerModule  │           │
│  │      (Android)      │    │       (iOS)         │           │
│  │       Kotlin        │    │       Swift         │           │
│  └─────────┬───────────┘    └─────────┬───────────┘           │
├────────────┼──────────────────────────┼────────────────────────┤
│            │      Native BLE Layer    │                         │
│  ┌─────────▼───────────┐    ┌─────────▼───────────┐           │
│  │   GATT Server       │    │   CBPeripheralMgr   │           │
│  │   + BLE Advertiser  │    │   + CBCentral        │           │
│  │   (Android API)     │    │   (CoreBluetooth)    │           │
│  └─────────┬───────────┘    └─────────┬───────────┘           │
├────────────┼──────────────────────────┼────────────────────────┤
│            │      Hardware Layer      │                         │
│  ┌─────────▼──────────────────────────▼───────────┐           │
│  │           Bluetooth Low Energy Radio            │           │
│  │      (IEEE 802.15.1, 2.4 GHz ISM Band)        │           │
│  └────────────────────────────────────────────────┘           │
└─────────────────────────────────────────────────────────────────┘

Key Components

ComponentTechnologyPurpose
UI LayerReact Native + ReanimatedAnimations, gesture handling, real-time state visualization
BleManagerTypeScript (Expo Module)State machine, packet protocol, sound sync, election algorithm
BleManagerModuleKotlin (Android) / Swift (iOS)Platform-specific BLE API access via Expo Modules
BlePeripheralManagerKotlin (Android) / Swift (iOS)GATT Server, advertising, characteristic management
react-native-ble-plxThird-party libraryClient-side BLE operations (scanning, connecting, reading)

BLE Mesh Topology

Server-Client Model

CAMDOM uses a leader election pattern where one device assumes the Server role and all other connected devices become Clients. The election is deterministic based on advertised numeric values.

stateDiagram-v2
    [*] --> Disconnected
    Disconnected --> Scanning : User taps Start
    Disconnected --> Server : No higher value found
    
    Scanning --> Client : Found higher value
    Scanning --> Server : 7.5s timeout / No higher value
    Scanning --> Disconnected : User cancels
    
    Client --> Synced : Connection established
    Client --> Disconnected : Connection failed
    
    Server --> Synced : First client connects
    Server --> Disconnected : User stops
    
    Synced --> Alarm : App backgrounded / Distance threshold
    Synced --> Migrating : Higher value server found
    Synced --> Disconnected : User disconnects
    
    Alarm --> Disconnected : Alarm stopped + all agree
    Migrating --> Client : Reconnect to new server
    Migrating --> Server : No new server found
    
    Connected --> Synced : Session established

Role Selection Algorithm

The server election uses a highest-value-wins protocol:

  1. Each device generates a random advertising value on startup
  2. Value ranges are platform-specific to prevent iOS↔iOS or Android↔Android conflicts:
    • iOS devices: 1–124 (client range), 125 (server threshold)
    • Android devices: 126–249 (client range), 251 (server threshold)
  3. During scanning, devices compare their local value with discovered remote values
  4. If remote > local: Connect to remote as client
  5. If local > remote (or no remote found): Become server with value 125 (iOS) or 251 (Android)
// BleManager.ts - Advertising value generation
async startAdvertising() {
    if (this.advertisingLocalValue != null && this.advertisingLocalValue <= 125) {
        return; // Already advertising as client or server
    }

    this.advertisingLocalValue =
        Platform.OS === "ios"
            ? this.randomIntFromInterval(1, 124)    // iOS client range
            : this.randomIntFromInterval(126, 249);  // Android client range

    this.distanceAlarmFired = false;

    try {
        this.module.bindServer(this.advertisingLocalValue.toString());
        return true;
    } catch (e) {
        await requestPermissions((granted) => {
            if (granted) {
                return this.startAdvertising();
            }
        });
        return false;
    }
}

Migration Protocol

When a new device with a higher value appears, the current server can migrate its role:

// Server detects higher-value device during scanning
if (this.isServer && remoteAdvertisedValue >= 126) {
    this.module.broadcastPacket(`m:d|${remoteAdvertisedValue.toString()}`);
    this.isMigratingConnection = true;
    this.isConnecting = false;
    this.connectedClients = [];
    // ... reset state
}

The m:d|{value} packet tells all clients to disconnect and reconnect to the new server.


Native Module Layer

Android: BleManagerModule.kt

The Android module uses Expo Modules (Kotlin DSL) to expose BLE functionality to JavaScript:

// modules/ble-manager/android/src/main/java/p4b/modules/blemanager/BleManagerModule.kt
class BleManagerModule : Module(), SingletonModule {
    private lateinit var peripheralManager: BlePeripheralManager
    
    override fun definition() = ModuleDefinition {
        Name("BleManager")
        
        OnCreate {
            peripheralManager = BlePeripheralManager(
                BlePeripheralManagerOptions(
                    appContext.reactContext?.applicationContext!!,
                    this@BleManagerModule
                )
            )
        }
        
        Events(
            "onStartGattServer",
            "onStopGattServer",
            "onStartAdvertising",
            "onStopAdvertising",
            "onDeviceConnected",
            "onDeviceDisconnected",
            "onConnectedDeviceStateChange",
            "onNotificationSent",
            "onPacketFromClient"
        )
        
        Function("bindServer") { name: String ->
            peripheralManager.updateAdvertisedName(name)
        }
        
        Function("unbindServer") { stopServer: Boolean ->
            peripheralManager.bleStopAdvertising(stopServer)
        }
        
        Function("broadcastPacket") { packet: String ->
            peripheralManager.broadcastPacket(packet.toByteArray())
        }
        
        Function("sendPacketToClient") { packet: String, to: String ->
            peripheralManager.sendPacketToClient(packet.toByteArray(), to)
        }
    }
}

Android: BlePeripheralManager.kt — GATT Server

The Android implementation uses the BluetoothGattServer API to host a GATT server:

// GATT Server initialization
private fun bleStartGattServer() {
    val gattServer = bluetoothManager.openGattServer(options.context, gattServerCallback)
    val service = BluetoothGattService(
        UUID.fromString(SERVICE_UUID),
        BluetoothGattService.SERVICE_TYPE_PRIMARY
    )
    
    val charForRead = BluetoothGattCharacteristic(
        UUID.fromString(CHAR_FOR_READ_UUID),
        BluetoothGattCharacteristic.PROPERTY_READ,
        BluetoothGattCharacteristic.PERMISSION_READ
    )
    
    val charForWrite = BluetoothGattCharacteristic(
        UUID.fromString(CHAR_FOR_WRITE_UUID),
        BluetoothGattCharacteristic.PROPERTY_WRITE,
        BluetoothGattCharacteristic.PERMISSION_WRITE
    )
    
    val charForIndicate = BluetoothGattCharacteristic(
        UUID.fromString(CHAR_FOR_INDICATE_UUID),
        BluetoothGattCharacteristic.PROPERTIES_INDICATE,
        BluetoothGattCharacteristic.PERMISSION_READ
    )
    
    val charConfigDescriptor = BluetoothGattDescriptor(
        UUID.fromString(CCC_DESCRIPTOR_UUID),
        BluetoothGattDescriptor.PERMISSION_READ or BluetoothGattDescriptor.PERMISSION_WRITE
    )
    charForIndicate.addDescriptor(charConfigDescriptor)
    
    service.addCharacteristic(charForRead)
    service.addCharacteristic(charForWrite)
    service.addCharacteristic(charForIndicate)
    
    gattServer.addService(service)
    this.gattServer = gattServer
}

iOS: BleManagerModule.swift

The iOS module mirrors the Android API using CoreBluetooth:

// modules/ble-manager/ios/BleManagerModule.swift
public class BleManagerModule: Module {
    private let peripheralManager: BlePeripheralManager = BlePeripheralManager.shared.self
    
    public func definition() -> ModuleDefinition {
        Name("BleManager")
        
        OnCreate {
            peripheralManager.bindEmitter(module: self)
        }
        
        Events(
            "onStartGattServer",
            "onStopGattServer",
            "onStartAdvertising",
            "onStopAdvertising",
            "onDeviceConnected",
            "onDeviceDisconnected",
            "onNotificationSent",
            "onPacketFromClient"
        )
        
        Function("bindServer") { (name: String) -> Void in
            peripheralManager.startAdvertising(deviceName: name)
        }
        
        Function("unbindServer") { (stopServer: Bool) -> Void in
            peripheralManager.bleStopAdvertising(stopServer: stopServer)
        }
        
        Function("broadcastPacket") { (packet: String) -> Void in
            peripheralManager.broadcastPacket(packet)
        }
        
        Function("sendPacketToClient") { (packet: String, to: String) -> Void in
            peripheralManager.sendPacketToClient(packet, deviceId: to)
        }
    }
}

iOS: BlePeripheralManager.swift — Peripheral Role

iOS uses CBPeripheralManager for the server role and CBCentral for client operations:

// GATT Service construction
private func buildBLEService() -> CBMutableService {
    let charForRead = CBMutableCharacteristic(type: uuidCharForRead,
                                              properties: .read,
                                              value: nil,
                                              permissions: .readable)
    
    let charForWrite = CBMutableCharacteristic(type: uuidCharForWrite,
                                               properties: .write,
                                               value: nil,
                                               permissions: .writeable)
    
    let charForIndicate = CBMutableCharacteristic(type: uuidCharForIndicate,
                                                  properties: .indicate,
                                                  value: nil,
                                                  permissions: .readable)
    
    self.charForIndicate = charForIndicate
    
    let service = CBMutableService(type: uuidService, primary: true)
    service.characteristics = [charForRead, charForWrite, charForIndicate]
    return service
}

// Advertising with device name containing numeric value
func startAdvertising(deviceName: String) {
    if isAdvertising {
        bleStopAdvertising(stopServer: false)
    }
    
    if(!isServicesBuilt){
        peripheralManager.add(buildBLEService())
    }
    
    self.advertisedName = deviceName
    peripheralManager.startAdvertising([
        CBAdvertisementDataLocalNameKey : deviceName,
        CBAdvertisementDataServiceUUIDsKey : [uuidService]
    ])
    isAdvertising = true
}

Service UUID and Characteristics

UUID Definitions

// BleManager.ts
const SERVICE_UUID = "25AE1441-05D3-4C5B-8281-93D4E07420CF";
const CHAR_FOR_READ_UUID = "25AE1442-05D3-4C5B-8281-93D4E07420CF";
const CHAR_FOR_WRITE_UUID = "25AE1443-05D3-4C5B-8281-93D4E07420CF";
const CHAR_FOR_INDICATE_UUID = "25AE1444-05D3-4C5B-8281-93D4E07420CF";
const CCC_DESCRIPTOR_UUID = "00002902-0000-1000-8000-00805f9b34fb";

Characteristic Roles

UUID SuffixNamePropertiesDirectionPurpose
...441ServicePrimary service container
...442CharForReadReadServer → ClientReturns advertised name (numeric value)
...443CharForWriteWriteClient → ServerClient sends packets to server
...444CharForIndicateIndicateServer → ClientServer broadcasts packets to clients
00002902CCCDRead/WriteClient Characteristic Configuration Descriptor

Data Flow

┌─────────────────────────────────────────────────────────────┐
│                    Client → Server                          │
│                                                             │
│  Client writes to CHAR_FOR_WRITE (base64 encoded packet)   │
│       │                                                     │
│       ▼                                                     │
│  Server receives in gattServerCallback                      │
│       │                                                     │
│       ▼                                                     │
│  Emitter sends "onPacketFromClient" event to TypeScript     │
└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│                    Server → Client(s)                        │
│                                                             │
│  TypeScript calls module.broadcastPacket(packet)            │
│       │                                                     │
│       ▼                                                     │
│  Native updates CHAR_FOR_INDICATE value                     │
│       │                                                     │
│       ▼                                                     │
│  All subscribed clients receive indication                  │
│       │                                                     │
│       ▼                                                     │
│  Client monitors CHAR_FOR_INDICATE, decodes packet          │
└─────────────────────────────────────────────────────────────┘

Advertising System

Advertising Data Structure

The advertising packet contains two components:

  1. Service UUID: Identifies CAMDOM devices (25AE1441-...)
  2. Local Name: The numeric advertising value (1–249)
// Android advertising configuration
val advertiseData = AdvertiseData.Builder()
    .setIncludeDeviceName(false)  // Service UUID only in main adv
    .addServiceUuid(ParcelUuid(UUID.fromString(SERVICE_UUID)))
    .build()

val scanResponse: AdvertiseData = AdvertiseData.Builder()
    .setIncludeDeviceName(true)  // Numeric value in scan response
    .build()

Platform-Specific Ranges

PlatformClient RangeServer ThresholdRationale
iOS1–124125iOS CoreBluetooth limitations
Android126–249251Android GATT Server flexibility

The gap between 125 and 126 ensures cross-platform disambiguation — an iOS server (125) is always lower than any Android client (126+), guaranteeing Android devices become servers when both platforms are present.

Advertising State Machine

stateDiagram-v2
    [*] --> Idle
    Idle --> Advertising : bindServer(value)
    Advertising --> Advertising : Value update (new value)
    Advertising --> Idle : unbindServer(true)
    
    note right of Advertising
        Device name = numeric value
        Service UUID in advertisement
        Scan response contains name
    end note

Packet Protocol

Packet Structure

All BLE communication uses a text-based, colon-delimited protocol encoded in base64 for transport:

{type}:{action}|{param1}|{param2}...

Packet Types

PrefixTypeDirectionPurpose
c:ConnectionClient → Server → ClientConnection handshake
s:SessionServer → Client / Client → ServerSession establishment
d:DisconnectionBothDisconnect request/response
m:MigrationServer → ClientsServer role migration
a:AlarmBothAlarm trigger/stop
sound:SoundServer → ClientsSound synchronization
n:NotificationServer → ClientsRSSI/notification packets
ping:PingClient → ServerKeepalive check
pong:PongServer → ClientKeepalive response
discq:Disconnect QueryClient → ServerDisconnect acknowledgment
disca:Disconnect AnswerServer → ClientDisconnect confirmation

Connection Handshake

sequenceDiagram
    participant C as Client
    participant S as Server
    
    Note over C,S: Phase 1: Connection
    C->>S: c:0:123 (platform:advertisingValue)
    S->>C: c:0:123 (echo back)
    
    Note over C,S: Phase 2: Session
    C->>S: s:r|1 (session request + fireOnDisconnect flag)
    S->>C: s:a|{sessionId} (session ID assignment)
    
    Note over C,S: Connection Established

Disconnection Protocol

sequenceDiagram
    participant C as Client
    participant S as Server
    
    Note over C,S: Mutual Disconnect Request
    C->>S: d:r|s (disconnect request - start)
    S->>S: Adds C to askedDisconnectionDevices
    
    alt All clients requested disconnect
        S->>C: d:a|a (disconnect approved)
        C->>S: d:r|1 (disconnect acknowledged)
        S->>C: d:a|a1 (disconnect final)
    else Partial disconnect
        S->>C: d:a|r (disconnect rejected)
    end

Alarm Protocol

sequenceDiagram
    participant C as Client
    participant S as Server
    
    Note over C,S: Alarm Trigger (app backgrounded)
    C->>S: a:u|i (alarm trigger - user initiated)
    S->>C: a:f|1|u (fire alarm - all units)
    
    Note over C,S: Alarm Stop
    C->>S: a:r|s (stop alarm request - start)
    S->>S: Adds C to askedToStopAlarmClients
    
    alt All clients agree to stop
        S->>C: a:f|0|a (alarm off - all units)
    end

Sound System

Sound Types

CAMDOM uses 5 distinct sounds, synchronized across all connected devices via BLE packets:

KeySoundTriggerLoopDescription
openOpening soundApp startNoConfirmation tone
pairPairing soundConnection establishedNoSuccess notification
unpairUnpairing soundDisconnectionNoDisconnect notification
alarmAlarm soundPrivacy breachYesContinuous alarm until stopped
pingPing soundKeepaliveNoProximity check tone

Sound Synchronization

// BleManager.ts - Sound playback with BLE broadcast
async playSound(key: AppSounds) {
    if (!this.isSoundsLoaded) return;
    
    // Ensure volume is at maximum
    if ((await VolumeManager.getVolume()).volume < 1) {
        await VolumeManager.showNativeVolumeUI({ enabled: true });
        await VolumeManager.setVolume(1);
    }
    
    // Loop alarm sound
    if (key === "alarm") {
        await this.loadedSounds.get(key)?.sound?.setIsLoopingAsync(true);
    }
    
    // Platform-specific playback
    if (Platform.OS === "ios") 
        this.loadedSounds.get(key)?.sound?.replayAsync();
    else 
        this.loadedSounds.get(key)?.sound?.playFromPositionAsync(0);
    
    // Server broadcasts to all clients (except alarm - already triggered locally)
    if (this.isServer && key !== "alarm") {
        this.module.broadcastPacket(`sound:1|${SoundsToPacketMapper(key)}`);
    }
}

Sound Mapping

// Packet to sound mapping
const PacketToSoundsMapper = (sound: string): AppSounds | undefined => {
    switch (sound) {
        case "0": return "open";
        case "1": return "pair";
        case "2": return "unpair";
        case "3": return "alarm";
        case "4": return "ping";
        default: return undefined;
    }
};

Audio Configuration

await Audio.setAudioModeAsync({
    staysActiveInBackground: true,
    playsInSilentModeIOS: true,
    interruptionModeIOS: InterruptionModeIOS.DoNotMix,
    interruptionModeAndroid: InterruptionModeAndroid.DoNotMix,
    shouldDuckAndroid: false,
    playThroughEarpieceAndroid: false,
});

Privacy by Design

Data Minimization

PrincipleImplementation
No data collectionZero analytics, telemetry, or user tracking
No cloud dependencyAll operations offline via BLE mesh
No persistent storageOnly session state in memory (MMKV for settings)
No network callsNo HTTP requests, no API endpoints
Encrypted BLEBLE 4.2+ with LE Secure Connections

GDPR Compliance

  • No personal data processed — only numeric advertising values
  • No third-party SDKs — no analytics, no ads, no tracking
  • Offline operation — no data leaves the device
  • User control — immediate disconnect capability
  • Transparent operation — visible BLE advertising name (numeric value)

Security Properties

  1. Proximity-based: Only devices within BLE range (~10m) can connect
  2. Mutual authentication: Both devices must agree on connection
  3. Session isolation: Each connection generates unique session ID
  4. No replay attacks: Session IDs are UUIDv4, single-use
  5. Hardware-level: Cannot be bypassed by software manipulation

Cross-Platform BLE Interop

The Challenge

Android and iOS have fundamentally different BLE architectures:

AspectAndroidiOS
Server roleBluetoothGattServerCBPeripheralManager
Client roleBluetoothGatt (connect)CBCentralManager (scan/connect)
AdvertisingBluetoothLeAdvertiserCBPeripheralManager.startAdvertising()
NotificationsnotifyCharacteristicChanged()updateValue(_:for:onSubscribedCentrals:)
Characteristic accessDirect UUID accessService discovery required

The Solution: Expo Modules

CAMDOM uses Expo Modules (formerly Expo Modules API) to create a unified TypeScript interface that abstracts platform differences:

// TypeScript abstraction
export default requireNativeModule('BleManager');

Both platforms expose identical functions:

  • bindServer(name: string) — Start GATT server/Peripheral with advertised name
  • unbindServer(stopServer: boolean) — Stop advertising, optionally remove services
  • broadcastPacket(packet: string) — Send to all subscribed centrals
  • sendPacketToClient(packet: string, to: string) — Send to specific device

Event Unification

Both platforms emit identical events:

  • onStartGattServer / onStopGattServer
  • onStartAdvertising / onStopAdvertising
  • onDeviceConnected / onDeviceDisconnected
  • onPacketFromClient / onNotificationSent

Platform-Specific Handling

Some operations require platform branching:

// iOS requires 400ms delay before scanning
setTimeout(async () => {
    await this.blePlxManager.startDeviceScan(...);
}, Platform.OS === "ios" ? 400 : 0);

// Platform-specific advertising values
this.advertisingLocalValue =
    Platform.OS === "ios"
        ? this.randomIntFromInterval(1, 124)
        : this.randomIntFromInterval(126, 249);

// Server threshold values
this.module.bindServer(Platform.OS === "ios" ? "125" : 251);

Connection State Machine

Complete State Diagram

stateDiagram-v2
    [*] --> Disconnected
    
    Disconnected --> Scanning : User initiates connection
    Disconnected --> Server : No higher value found during scan
    
    Scanning --> Client : Higher value found
    Scanning --> Server : 7.5s timeout
    Scanning --> Disconnected : User cancels / Error
    
    Client --> Connecting : device.connect()
    Client --> Disconnected : Connection failed
    
    Connecting --> Synced : Connection established + session
    Connecting --> Disconnected : Error / Timeout
    
    Server --> Synced : First client connects + session
    
    Synced --> Alarm : App backgrounded / Distance threshold
    Synced --> Migrating : Higher value server found
    Synced --> Disconnected : User stops / Error
    Synced --> Connected : Intermediate state
    
    Connected --> Synced : Session established
    
    Alarm --> Disconnected : All clients agree to stop
    Alarm --> Disconnected : User force disconnects
    
    Migrating --> Client : Reconnect to new server
    Migrating --> Server : No new server found
    
    note right of Scanning
        Scanning for SERVICE_UUID
        Comparing advertised values
        7.5s timeout if nothing found
    end note
    
    note right of Synced
        Fully operational state
        Alarm monitoring active
        RSSI tracking enabled
    end note

State Descriptions

StateDescriptionUI StateActive Operations
disconnectedInitial state, no BLE activityIdleNone
scanningLooking for servers to connectLoadingBLE scan active
connectingAttempting to connect to serverLoadingDevice connection
connectedConnected, awaiting sessionTransitionWaiting for session ID
syncedFully operational, session activeProtectedAlarm monitoring, RSSI
migratingSwitching to new serverTransitionDisconnect + reconnect
alarmPrivacy breach detectedAlertAlarm sound playing

State Transitions

// Key state transitions in BleManager.ts

// Scanning → Client (found higher value)
this.sharedValues.appState.value = "connecting";

// Client → Synced (session established)
this.sharedValues.appState.value = "synced";

// Synced → Alarm (app backgrounded)
if (this.sharedValues.appState.value === "synced") {
    if (this.isServer) {
        this.sharedValues.appState.value = "alarm";
        this.module.broadcastPacket("a:f|1|u");
    } else {
        this.sendPacketToServer("a:u|i");
    }
}

// Alarm → Disconnected (all agree to stop)
this.module.broadcastPacket("a:f|0|a");
this.sharedValues.appState.value = "disconnected";

RSSI Proximity Detection

Overview

CAMDOM implements RSSI-based proximity detection to trigger alarms when devices move beyond a configurable threshold.

Implementation

// Client-side RSSI reading
private configureClientRSSITimeout() {
    return setTimeout(async () => {
        if (this.connectedDevice) {
            const updatedWithRssiConnectedDevice =
                await this.connectedDevice.readRSSI();
            if (updatedWithRssiConnectedDevice?.rssi !== null) {
                const sense = storage.getNumber(StorageKeys.rssiSense);
                // Send RSSI + sensitivity to server
                await this.sendPacketToServer(
                    `a:u|rssi|${updatedWithRssiConnectedDevice.rssi}${sense !== undefined ? `|${sense.toFixed(0)}` : "null"}`
                );
            }
        }
    }, 15);  // 45 fps tick rate
}

Server-Side Processing

private handleRssiUpdateFromClient(rssi: number, from: string) {
    if (this.rssiUpdatesFromClients.has(from)) {
        let lastPackets = this.rssiUpdatesFromClients.get(from);
        if (!lastPackets) lastPackets = [];
        
        // Rolling window of 5 readings
        if (lastPackets.length > 5) {
            lastPackets.shift();
        }
        
        lastPackets.push(rssi);
        this.rssiUpdatesFromClients.set(from, lastPackets);
    } else {
        this.rssiUpdatesFromClients.set(from, [rssi]);
    }
}

Sensitivity Configuration

Users can adjust proximity sensitivity via the app settings:

  • Low sensitivity: Larger distance threshold (e.g., -70 dBm)
  • High sensitivity: Smaller distance threshold (e.g., -50 dBm)

The server tracks the highest sensitivity among all connected clients to ensure the most restrictive threshold is applied.


Build Configuration

React Native / Expo

{
  "expo": {
    "name": "Camdom",
    "slug": "innocean-camdom",
    "version": "4.4.44",
    "android": {
      "package": "com.billyboy.condomapp"
    },
    "plugins": [
      "expo-router",
      ["expo-font", { "fonts": ["..."] }],
      "expo-localization",
      ["react-native-ble-plx", {
        "isBackgroundEnabled": true,
        "modes": ["peripheral", "central"]
      }]
    ]
  }
}

Key Dependencies

PackageVersionPurpose
react-native0.82.1Core framework
expo^54.0.23Module system
react-native-ble-plx^3.5.0Client BLE operations
expo-modules-core(bundled)Native module bridge
expo-av~16.0.7Audio playback
react-native-reanimated~4.1.5Animations
react-native-mmkv4.0.0Fast key-value storage

Technical Specifications

BLE Parameters

ParameterValueNotes
BLE Version4.2+LE Secure Connections required
MTU53 bytesRequested during connection
Scan ModeLow LatencyMaximum discovery rate
Advertising IntervalLow Latency mode~100ms
TX PowerHighMaximum range
Connection IntervalDefault~7.5ms minimum

Packet Size Constraints

Due to BLE MTU limitations, packets must be concise:

  • Maximum packet size: ~50 bytes (after MTU overhead)
  • Encoding: Base64 for write operations
  • Format: Text-based, colon-delimited

Performance Metrics

MetricTargetAchieved
Connection time< 2s~1.5s
Alarm latency< 500ms~200ms
Battery drain< 5%/hour~3%/hour
Range10m10-15m (line of sight)

Future Considerations

Potential Enhancements

  1. Mesh networking: Multi-hop relay between non-adjacent devices
  2. Group protection: Multiple device pairs with shared alarm
  3. Geofencing: GPS integration for location-based alerts
  4. Haptic feedback: Vibration patterns for silent alerts
  5. Encryption layer: Application-level encryption on top of BLE

Known Limitations

  1. BLE range: Physical limitation of ~10m
  2. Cross-platform latency: Android↔iOS slightly higher than same-platform
  3. Background restrictions: iOS limits BLE operations in background
  4. No persistence: Session state lost on app restart (by design)

References

  • Repository: condom-app/ (private)
  • Native modules: modules/ble-manager/
  • Android GATT: modules/ble-manager/android/src/main/java/p4b/modules/blemanager/
  • iOS Peripheral: modules/ble-manager/ios/
  • TypeScript manager: modules/ble-manager/src/BleManager.ts

This architecture represents a novel approach to digital consent protection — using hardware-level BLE mesh networking to create an unbreakable proximity tether. The system operates entirely offline, collects zero data, and provides real-time protection through synchronized alarms.