-
Notifications
You must be signed in to change notification settings - Fork 0
Home
Diagnostic and testing features for Zelara development and device communication validation.
- 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
- 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
- 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
- WebSocket connection status
- Linked device count (auto-updates via Tauri events)
- Token validation testing
- Mobile: React Native components (
react-native-vision-camerafor capture,react-native-fsfor 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)
- Navigate to Testing Module from home screen
- Check connection status (must be linked to Desktop first — scan QR in Device Pairing)
- Tap Take Photo and capture any image with the camera
- Tap Run Image Inversion Test — sends the photo to Desktop
- View original and color-inverted images side-by-side in the result modal
- Generate QR code in Device Pairing — server starts automatically
- Once mobile scans and links, the Testing panel's device count updates instantly (no refresh needed)
- When mobile sends an image inversion test, the original and inverted images appear in the Last Inversion Test section
- The test log records timestamps and device addresses for each event
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-linkedis 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).
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.
- Mobile:
DevicePairingScreencallsDeviceLinkingService.connect()→ws.onopenfires - Mobile:
DeviceLinkingService.sendHandshake()sendshandshakewith token anddevice_id - Desktop Rust: verifies token → checks if
device_idalready known:-
New device: adds to
linked_devices, emitsdevice-linked - Known device (reconnect): updates name in-place, no event emitted
-
New device: adds to
- Desktop Rust: returns
{ "message": "Handshake successful" } - Desktop React:
device-linkedfires (first pair only) →DevicePairing.tsxupdates list,TestingPanel.tsxincrements count + logs,App.tsxshows toast - 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"
}When a connection drops (network loss, airplane mode, app backgrounded):
- Desktop Rust: WebSocket loop exits on
Message::Closeor read error →retain()removes device fromlinked_devices→ emitsdevice-disconnectedevent - Desktop React:
device-disconnected→TestingPanel.tsxdecrements device count + logs entry - Mobile:
ws.oncloseorws.onerrorfires →connected = false,connection = null→notifyConnectionChange(false)→scheduleReconnect() - Mobile
scheduleReconnect(): waits 2 s (then 4 s, 8 s … max 30 s), retriesconnect()+sendHandshake()using stored credentials - On success:
reconnectAttemptresets 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.
- Mobile:
TestingScreencallstakePhoto()→ camera captures photo → file path stored - Mobile:
RNFS.readFile(path, 'base64')converts photo to base64 string - Mobile:
DeviceLinkingService.sendImageInversionTest(base64Image)sends WebSocket task - Desktop Rust:
device_linking.rsreceivesimage_inversion_testtask, callsinvert_image() - Desktop Rust:
imagecrate decodes base64 → inverts pixel colors → re-encodes to PNG - Desktop Rust: Emits
image-inversion-resultTauri event → Desktop React UI updates instantly - Desktop Rust: Sends WebSocket response with
invertedImageback to mobile - Mobile: Receives response, displays original + inverted side-by-side in modal
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"
}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.
- Mobile:
isConnectedstate becomestrueafter pairing - Mobile:
useEffectonisConnectedstarts asetInterval(1 second) - Mobile: Each tick increments
counterRef.current, updates display state, callsDeviceLinkingService.sendCounterUpdate(value)(fire-and-forget — errors are swallowed silently) - Desktop Rust: Receives
counter_updatetask, emitscounter-updateTauri event with{ value } - Desktop React:
listen('counter-update', ...)updatescounterstate → display re-renders - Mobile: When
isConnectedbecomesfalse, 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"
}-
Mobile side:
- Add test button to
TestingScreen.tsx - Implement test function that calls
DeviceLinkingService - Handle response and display results
- Add test button to
-
Desktop side:
- Add new task type handler in
device_linking.rs - Implement processing logic
- Return
TaskResponsewith results
- Add new task type handler in
-
Service layer:
- Add method to
DeviceLinkingService.ts - Follow promise pattern with pending requests
- Set timeout for long-running operations
- Add method to
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);
});
}- 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