Skip to content
HlexNC edited this page Mar 14, 2026 · 7 revisions

Testing Module Wiki

Purpose

Diagnostic and testing features for Zelara development and device communication validation.

Features

Desktop Clock

  • Desktop testing panel shows the current device time, updating every second
  • Visible immediately — no mobile connection required
  • Verifies the desktop UI is rendering and reacting to state changes

Live Counter (Mobile → Desktop)

  • While mobile is connected to Desktop, an auto-incrementing counter ticks every second on mobile
  • Each value is sent via WebSocket and displayed on the Desktop testing panel in real time
  • Resets to 0 when the connection drops or is not yet established
  • Together with the clock, proves both the WebSocket link and the desktop UI are live

Image Inversion Test (Camera-Based)

  • Tests full round-trip image processing pipeline using a real camera photo
  • Mobile takes photo → sends to Desktop → Desktop inverts colors → result appears on both devices
  • Validates WebSocket communication, base64 encoding, and Tauri event delivery

Connection Diagnostics

  • WebSocket connection status
  • Linked device count (auto-updates via Tauri events)
  • Token validation testing

Architecture

  • Mobile: React Native components (react-native-vision-camera for capture, react-native-fs for base64 read)
  • Desktop: Tauri + React components (listens to Tauri events for real-time updates)
  • Communication: WebSocket via DeviceLinkingService (request/response) + Tauri events (server → UI push)

Usage

Mobile App

  1. Navigate to Testing Module from home screen
  2. Check connection status (must be linked to Desktop first — scan QR in Device Pairing)
  3. Tap Take Photo and capture any image with the camera
  4. Tap Run Image Inversion Test — sends the photo to Desktop
  5. View original and color-inverted images side-by-side in the result modal

Desktop App

  1. Generate QR code in Device Pairing — server starts automatically
  2. Once mobile scans and links, the Testing panel's device count updates instantly (no refresh needed)
  3. When mobile sends an image inversion test, the original and inverted images appear in the Last Inversion Test section
  4. The test log records timestamps and device addresses for each event

Tauri Event Channels

The Desktop frontend listens for Tauri events emitted by device_linking.rs:

Event Payload When
device-linked { id, name, platform, discovery_method } A new mobile device completes handshake (discovery_method: "ble" or "qr")
device-disconnected { id: string, discovery_method: string } A mobile device disconnects (clean close or network drop)
image-inversion-result { original, inverted, device, timestamp } Desktop finishes an image_inversion_test task
counter-update { value: number } Every second while mobile counter is running
ble-status-changed { status: "advertising" | "idle" | "notSupported" | "error", ip?, port?, message? } BLE advertising state changes (start, stop, error)

Both DevicePairing.tsx and TestingPanel.tsx subscribe to device-linked and device-disconnected. Only TestingPanel.tsx subscribes to image-inversion-result and counter-update.

Note: device-linked is only emitted for genuinely new device IDs. If a known device reconnects after a network drop, the entry is updated in-place and no event is fired (device count does not change).

Technical Details

Handshake Flow

The handshake is the first message sent from mobile immediately after the WebSocket connection is established. It triggers device registration on the Desktop and fires the device-linked Tauri event — ensuring the Desktop UI updates the moment the user scans the QR code, not later when counter or image tasks begin.

The handshake also carries a stable device_id (UUID persisted in AsyncStorage) so the Desktop can deduplicate reconnects — if the same device reconnects after a network drop, the entry is updated in-place instead of creating a new one.

  1. Mobile: DevicePairingScreen calls DeviceLinkingService.connect()ws.onopen fires
  2. Mobile: DeviceLinkingService.sendHandshake() sends handshake with token and device_id
  3. Desktop Rust: verifies token → checks if device_id already known:
    • New device: adds to linked_devices, emits device-linked
    • Known device (reconnect): updates name in-place, no event emitted
  4. Desktop Rust: returns { "message": "Handshake successful" }
  5. Desktop React: device-linked fires (first pair only) → DevicePairing.tsx updates list, TestingPanel.tsx increments count + logs, App.tsx shows toast
  6. Mobile: sendHandshake() resolves → "Device Linked!" alert shown

WebSocket message (Mobile → Desktop):

{
  "taskId": "task_1234567890_abc123",
  "taskType": "handshake",
  "payload": {
    "token": "pairing_token",
    "device_id": "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx",
    "discovery_method": "qr"
  },
  "timestamp": "2026-03-01T12:00:00Z"
}

discovery_method is "ble" when Mobile connected via BLE auto-discovery, "qr" (default) when via QR scan. BLE connections skip token validation (proximity = trust); the session is flagged as trusted for its entire duration.

WebSocket response (Desktop → Mobile):

{
  "taskId": "task_1234567890_abc123",
  "success": true,
  "result": { "message": "Handshake successful" },
  "timestamp": "2026-03-01T12:00:00Z"
}

Disconnect & Auto-Reconnect Flow

When a connection drops (network loss, airplane mode, app backgrounded):

  1. Desktop Rust: WebSocket loop exits on Message::Close or read error → retain() removes device from linked_devices → emits device-disconnected event
  2. Desktop React: device-disconnectedTestingPanel.tsx decrements device count + logs entry
  3. Mobile: ws.onclose or ws.onerror fires → connected = false, connection = nullnotifyConnectionChange(false)scheduleReconnect()
  4. Mobile scheduleReconnect(): waits 2 s (then 4 s, 8 s … max 30 s), retries connect() + sendHandshake() using stored credentials
  5. On success: reconnectAttempt resets to 0, notifyConnectionChange(true) → UI shows connected again

DeviceLinkingService.disconnect() (explicit user action) cancels any pending reconnect timer and clears stored credentials — preventing auto-reconnect after an intentional disconnect.


Image Inversion Flow

  1. Mobile: TestingScreen calls takePhoto() → camera captures photo → file path stored
  2. Mobile: RNFS.readFile(path, 'base64') converts photo to base64 string
  3. Mobile: DeviceLinkingService.sendImageInversionTest(base64Image) sends WebSocket task
  4. Desktop Rust: device_linking.rs receives image_inversion_test task, calls invert_image()
  5. Desktop Rust: image crate decodes base64 → inverts pixel colors → re-encodes to PNG
  6. Desktop Rust: Emits image-inversion-result Tauri event → Desktop React UI updates instantly
  7. Desktop Rust: Sends WebSocket response with invertedImage back to mobile
  8. Mobile: Receives response, displays original + inverted side-by-side in modal

WebSocket Protocol

Request:

{
  "taskId": "task_1234567890_abc123",
  "taskType": "image_inversion_test",
  "payload": {
    "imageData": "base64_encoded_png...",
    "token": "pairing_token"
  },
  "timestamp": "2025-01-15T12:00:00Z"
}

Response:

{
  "success": true,
  "result": {
    "invertedImage": "base64_encoded_png...",
    "message": "Image inverted successfully"
  },
  "timestamp": "2025-01-15T12:00:01Z"
}

BLE Auto-Discovery

Desktop advertises its IP and port over Bluetooth Low Energy. Mobile scans for Zelara BLE advertisements and auto-connects — no QR code scan needed. QR pairing remains a fallback.

When BLE is available, the animation in the Testing Panel shows a live packet travelling from the Desktop node to the Mobile node whenever a device is connected via BLE. When no BLE connection is active (QR-only or BLE unavailable), the animation is hidden.

Discovery method tracking: Each connected device carries a discovery_method field ("ble" or "qr"). The animation activates only for BLE-discovered connections.

BLE status badge: The panel shows one of:

  • BLE Advertising (green) — Desktop is broadcasting its IP over BLE
  • BLE Idle (gray) — BLE supported but not currently advertising
  • BLE Not Supported (gray) — No BLE adapter present
  • BLE Error (red) — BLE available but failed to start; message shown

Platform support: Windows (WinRT BluetoothLEAdvertisementPublisher). macOS/Linux return "Not Supported" (pluggable via #[cfg] in future).

See Device-Linking Architecture for the BLE protocol spec.


Counter Flow

  1. Mobile: isConnected state becomes true after pairing
  2. Mobile: useEffect on isConnected starts a setInterval (1 second)
  3. Mobile: Each tick increments counterRef.current, updates display state, calls DeviceLinkingService.sendCounterUpdate(value) (fire-and-forget — errors are swallowed silently)
  4. Desktop Rust: Receives counter_update task, emits counter-update Tauri event with { value }
  5. Desktop React: listen('counter-update', ...) updates counter state → display re-renders
  6. Mobile: When isConnected becomes false, effect cleanup clears the interval and resets counter to 0

WebSocket message (Mobile → Desktop):

{
  "taskId": "task_1234567890_abc123",
  "taskType": "counter_update",
  "payload": {
    "value": 42,
    "token": "pairing_token"
  },
  "timestamp": "2025-01-15T12:00:00Z"
}

WebSocket response (Desktop → Mobile):

{
  "taskId": "task_1234567890_abc123",
  "success": true,
  "result": { "received": 42 },
  "timestamp": "2025-01-15T12:00:00Z"
}

Development

Adding New Tests

  1. Mobile side:

    • Add test button to TestingScreen.tsx
    • Implement test function that calls DeviceLinkingService
    • Handle response and display results
  2. Desktop side:

    • Add new task type handler in device_linking.rs
    • Implement processing logic
    • Return TaskResponse with results
  3. Service layer:

    • Add method to DeviceLinkingService.ts
    • Follow promise pattern with pending requests
    • Set timeout for long-running operations

Example: Adding Echo Test

Mobile (TestingScreen.tsx):

const testEcho = async () => {
  const result = await DeviceLinkingService.sendEchoTest("Hello!");
  Alert.alert('Echo', result.echo);
};

Desktop (device_linking.rs):

"echo_test" => {
    let message = payload["message"].as_str().unwrap_or("");
    TaskResponse {
        success: true,
        result: serde_json::json!({
            "echo": message
        }),
        timestamp: chrono::Utc::now().to_rfc3339(),
    }
}

Service (DeviceLinkingService.ts):

async sendEchoTest(message: string): Promise<any> {
  if (!this.isConnected()) {
    throw new Error('Not connected to Desktop');
  }

  const taskId = `task_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
  const request: TaskRequest = {
    taskId,
    taskType: 'echo_test',
    payload: { message, token: this.connectionInfo!.token },
    timestamp: new Date().toISOString(),
  };

  return new Promise((resolve, reject) => {
    this.pendingRequests.set(taskId, (response: TaskResponse) => {
      if (response.success) {
        resolve(response.result);
      } else {
        reject(new Error(response.result.error || 'Echo failed'));
      }
    });
    this.connection!.send(JSON.stringify(request));

    setTimeout(() => {
      if (this.pendingRequests.has(taskId)) {
        this.pendingRequests.delete(taskId);
        reject(new Error('Request timeout'));
      }
    }, 10000);
  });
}

Future Enhancements

  • Network latency measurements (round-trip time for counter packets)
  • Performance benchmarking tests
  • Battery usage monitoring
  • Memory leak detection
  • Automated test suite
  • Test result history/logging