import * as React from 'react'; import { ActivityIndicator, Image, Pressable, ScrollView, Text, TextInput, View } from 'react-native'; import { rotateImage } from '../modules/imaging'; import { toBase64Image } from '../utils/base64'; import { Agent } from '../agent/Agent'; import { InvalidateSync } from '../utils/invalidateSync'; import { textToSpeech } from '../modules/openai'; function usePhotos(device: BluetoothRemoteGATTServer) { // Subscribe to device const [photos, setPhotos] = React.useState>([]); const [subscribed, setSubscribed] = React.useState(false); React.useEffect(() => { (async () => { let previousChunk = -1; let buffer: Uint8Array = new Uint8Array(0); function onChunk(id: number | null, data: Uint8Array) { // Resolve if packet is the first one if (previousChunk === -1) { if (id === null) { return; } else if (id === 0) { previousChunk = 0; buffer = new Uint8Array(0); } else { return; } } else { if (id === null) { console.log('Photo received', buffer); const timestamp = Date.now(); // Get current timestamp rotateImage(buffer, '270').then((rotated) => { console.log('Rotated photo', rotated); setPhotos((p) => [...p, { data: rotated, timestamp: timestamp }]); // Store data and timestamp }); previousChunk = -1; return; } else { if (id !== previousChunk + 1) { previousChunk = -1; console.error('Invalid chunk', id, previousChunk); return; } previousChunk = id; } } // Append data buffer = new Uint8Array([...buffer, ...data]); } // Subscribe for photo updates const service = await device.getPrimaryService('19B10000-E8F2-537E-4F6C-D104768A1214'.toLowerCase()); const photoCharacteristic = await service.getCharacteristic('19b10005-e8f2-537e-4f6c-d104768a1214'); await photoCharacteristic.startNotifications(); setSubscribed(true); photoCharacteristic.addEventListener('characteristicvaluechanged', (e) => { let value = (e.target as BluetoothRemoteGATTCharacteristic).value!; let array = new Uint8Array(value.buffer); if (array[0] == 0xff && array[1] == 0xff) { onChunk(null, new Uint8Array()); } else { let packetId = array[0] + (array[1] << 8); let packet = array.slice(2); onChunk(packetId, packet); } }); // Start automatic photo capture every 5s const photoControlCharacteristic = await service.getCharacteristic('19b10006-e8f2-537e-4f6c-d104768a1214'); await photoControlCharacteristic.writeValue(new Uint8Array([0x05])); })(); }, []); return [subscribed, photos] as const; } export const DeviceView = React.memo((props: { device: BluetoothRemoteGATTServer }) => { const [subscribed, photos] = usePhotos(props.device); const agent = React.useMemo(() => new Agent(), []); const agentState = agent.use(); const [activePhotoIndex, setActivePhotoIndex] = React.useState(null); // Background processing agent const processedPhotos = React.useRef([]); const sync = React.useMemo(() => { let processed = 0; return new InvalidateSync(async () => { if (processedPhotos.current.length > processed) { let unprocessed = processedPhotos.current.slice(processed); processed = processedPhotos.current.length; await agent.addPhoto(unprocessed); } }); }, []); React.useEffect(() => { processedPhotos.current = photos.map(p => p.data); sync.invalidate(); }, [photos]); return ( {/* Display photos in a grid filling the screen */} {photos.slice().reverse().map((photo, index) => ( // Display newest first setActivePhotoIndex(photos.length - 1 - index)} onPressOut={() => setActivePhotoIndex(null)} style={{ position: 'relative', width: '33%', // Roughly 3 images per row aspectRatio: 1, // Make images square padding: 2 // Add spacing }} > {activePhotoIndex === (photos.length - 1 - index) && ( {new Date(photo.timestamp).toLocaleTimeString()} )} ))} ); });