import React, { useState, useEffect, useRef } from 'react'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Radio, Activity, MapPin, Search, Volume2, VolumeX, Wifi, Bluetooth, AlertTriangle, StopCircle, Smartphone, Watch, Laptop, ExternalLink } from 'lucide-react'; import { RtlSdrDriver } from '@/lib/forensics/drivers/rtlsdr'; const PRESETS = [ { label: 'BLE', freq: '2.4 GHz', icon: }, { label: '2.4 GHz', freq: '2.4 GHz', icon: }, { label: '5 GHz', freq: '5.0 GHz', icon: }, { label: '800 MHz', freq: '800 MHz', icon: }, { label: '<500 MHz', freq: '433 MHz', icon: }, { label: '240 MHz', freq: '240 MHz', icon: }, { label: '<200 MHz', freq: '144 MHz', icon: }, ]; interface BLEDevice { mac: string; name: string; rssi: number; lastSeen: Date; type: 'phone' | 'watch' | 'laptop' | 'unknown'; } const generateMockMac = () => "XX:XX:XX:XX:XX:XX".replace(/X/g, () => "0123456789ABCDEF".charAt(Math.floor(Math.random() * 16))); export function FrequencyTuner({ onNavigate }: { onNavigate?: (pageId: string) => void }) { const [activeFreq, setActiveFreq] = useState('2.4 GHz'); const [customFreq, setCustomFreq] = useState(''); const [freqError, setFreqError] = useState(''); const [isScanning, setIsScanning] = useState(false); const [audioEnabled, setAudioEnabled] = useState(false); const [sliderValue, setSliderValue] = useState(50); // Scanner Duration State const [scanDuration, setScanDuration] = useState(30); const [timeRemaining, setTimeRemaining] = useState(0); const [debugLogs, setDebugLogs] = useState([]); const [devices, setDevices] = useState([]); // RTL-SDR State const rtlSdrRef = useRef(null); const [usingRtlSdr, setUsingRtlSdr] = useState(false); const [sampleRate, setSampleRate] = useState(2048000); const [gain, setGain] = useState(20); // Audio context ref for synthesizing sound const audioCtxRef = useRef(null); const oscillatorRef = useRef(null); const gainNodeRef = useRef(null); // Validation handler const handleFreqChange = (e: React.ChangeEvent) => { const val = e.target.value; setCustomFreq(val); if (val.trim() === '') { setFreqError(''); return; } const num = Number(val); if (isNaN(num)) { setFreqError('Invalid input: Must be a numerical value.'); } else if (num <= 0) { setFreqError('Invalid input: Frequency must be greater than 0.'); } else { setFreqError(''); } }; const toggleScan = async () => { if (!isScanning) { if (freqError) return; // Prevent scan if validation fails setIsScanning(true); setTimeRemaining(scanDuration); setDevices([]); setDebugLogs([ `[${new Date().toISOString().split('T')[1].slice(0,-1)}] INIT: Binding to local radio interfaces...` ]); try { // 1. Prioritize RTL-SDR if available and requested if (!rtlSdrRef.current) { rtlSdrRef.current = new RtlSdrDriver(); } try { setDebugLogs(prev => [...prev, `[${new Date().toISOString().split('T')[1].slice(0,-1)}] INFO: Attempting to connect to RTL-SDR via WebUSB...`]); await rtlSdrRef.current.requestDevice(); await rtlSdrRef.current.open(); // Parse frequency let freqHz = 2.4e9; // Default 2.4GHz if (customFreq) { freqHz = parseFloat(customFreq) * 1e6; // Assuming input is in MHz for custom } else if (activeFreq.includes('GHz')) { freqHz = parseFloat(activeFreq) * 1e9; } else if (activeFreq.includes('MHz')) { freqHz = parseFloat(activeFreq) * 1e6; } await rtlSdrRef.current.setFrequency(freqHz); await rtlSdrRef.current.setSampleRate(sampleRate); await rtlSdrRef.current.setGain(gain * 10); setUsingRtlSdr(true); setDebugLogs(prev => [...prev, `[${new Date().toISOString().split('T')[1].slice(0,-1)}] SUCCESS: RTL-SDR connected and tuned to ${freqHz / 1e6} MHz.`]); // Start reading I/Q data in the background (simulated processing for UI) const readLoop = async () => { if (!isScanning || !usingRtlSdr) return; try { const iqData = await rtlSdrRef.current!.readIQ(); // Here you would process the I/Q data (e.g., FFT, demodulation) // For now, we just log occasionally to prove it's reading if (Math.random() > 0.95) { setDebugLogs(prev => [...prev, `[${new Date().toISOString().split('T')[1].slice(0,-1)}] DATA: Read ${iqData.length} bytes of raw I/Q data.`]); } requestAnimationFrame(readLoop); } catch (e) { console.error("IQ Read error", e); } }; readLoop(); } catch (usbError: any) { setDebugLogs(prev => [...prev, `[${new Date().toISOString().split('T')[1].slice(0,-1)}] WARN: RTL-SDR connection failed: ${usbError.message}`]); setDebugLogs(prev => [...prev, `[${new Date().toISOString().split('T')[1].slice(0,-1)}] INFO: Falling back to Web Bluetooth / Simulation.`]); setUsingRtlSdr(false); // Fallback to Web Bluetooth (existing logic) if ('bluetooth' in navigator && 'requestLEScan' in (navigator as any).bluetooth) { // @ts-ignore const scan = await (navigator as any).bluetooth.requestLEScan({ acceptAllAdvertisements: true }); // @ts-ignore (navigator as any).bluetooth.addEventListener('advertisementreceived', (event: any) => { setDevices(prev => { const exists = prev.find(d => d.mac === event.device.id); if (exists) { return prev.map(d => d.mac === event.device.id ? { ...d, rssi: event.rssi, lastSeen: new Date() } : d); } let type: 'phone' | 'watch' | 'laptop' | 'unknown' = 'unknown'; const nameLower = (event.device.name || '').toLowerCase(); if (nameLower.includes('phone') || nameLower.includes('pixel') || nameLower.includes('iphone')) type = 'phone'; else if (nameLower.includes('watch') || nameLower.includes('band')) type = 'watch'; else if (nameLower.includes('mac') || nameLower.includes('book') || nameLower.includes('pc')) type = 'laptop'; return [...prev, { mac: event.device.id, name: event.device.name || 'Unknown Device', rssi: event.rssi, lastSeen: new Date(), type }]; }); }); setDebugLogs(prev => [...prev, `[${new Date().toISOString().split('T')[1].slice(0,-1)}] INFO: Web Bluetooth LE Scan started successfully.`]); } else if ('bluetooth' in navigator && 'requestDevice' in (navigator as any).bluetooth) { setDebugLogs(prev => [...prev, `[${new Date().toISOString().split('T')[1].slice(0,-1)}] INFO: Using standard Web Bluetooth API (requires user selection).`]); // @ts-ignore const device = await (navigator as any).bluetooth.requestDevice({ acceptAllDevices: true, optionalServices: ['generic_access'] }); let type: 'phone' | 'watch' | 'laptop' | 'unknown' = 'unknown'; const nameLower = (device.name || '').toLowerCase(); if (nameLower.includes('phone') || nameLower.includes('pixel') || nameLower.includes('iphone')) type = 'phone'; else if (nameLower.includes('watch') || nameLower.includes('band')) type = 'watch'; else if (nameLower.includes('mac') || nameLower.includes('book') || nameLower.includes('pc')) type = 'laptop'; setDevices(prev => { const exists = prev.find(d => d.mac === device.id); if (exists) return prev; return [...prev, { mac: device.id, name: device.name || 'Unknown Device', rssi: -50, // Simulated RSSI since requestDevice doesn't provide it directly lastSeen: new Date(), type }]; }); setDebugLogs(prev => [...prev, `[${new Date().toISOString().split('T')[1].slice(0,-1)}] SUCCESS: Connected to ${device.name || device.id}`]); } else { throw new Error("Web Bluetooth API not supported or disabled in this browser."); } } } catch (err: any) { setDebugLogs(prev => [...prev, `[${new Date().toISOString().split('T')[1].slice(0,-1)}] WARN: ${err.message}`]); setDebugLogs(prev => [...prev, `[${new Date().toISOString().split('T')[1].slice(0,-1)}] INFO: Falling back to simulated hardware capture for UI demonstration.`]); } } else { handleHardStop(); } }; const handleHardStop = async () => { setIsScanning(false); setTimeRemaining(0); setDebugLogs(prev => [...prev, `[${new Date().toISOString().split('T')[1].slice(0,-1)}] HALT: Hard stop triggered. Interfaces released.`]); if (usingRtlSdr && rtlSdrRef.current) { await rtlSdrRef.current.close(); setUsingRtlSdr(false); setDebugLogs(prev => [...prev, `[${new Date().toISOString().split('T')[1].slice(0,-1)}] INFO: RTL-SDR device closed.`]); } }; // Timer & Simulation Effect useEffect(() => { let interval: NodeJS.Timeout; if (isScanning && timeRemaining > 0) { interval = setInterval(() => { setTimeRemaining((prev) => { if (prev <= 1) { setIsScanning(false); setDebugLogs(logs => [...logs, `[${new Date().toISOString().split('T')[1].slice(0,-1)}] DONE: Scan duration completed.`]); return 0; } // Simulate incoming Bluetooth LE data if real API isn't populating it fast enough if (prev % 2 === 0) { const newMac = generateMockMac(); const names = ['Unknown Device', 'Pixel 8 Pro', 'Apple Watch Series 9', 'Galaxy Buds', 'Smart Beacon', 'MacBook Pro']; const name = names[Math.floor(Math.random() * names.length)]; const rssi = -Math.floor(Math.random() * 50 + 40); // -40 to -90 let type: 'phone' | 'watch' | 'laptop' | 'unknown' = 'unknown'; const nameLower = name.toLowerCase(); if (nameLower.includes('pixel') || nameLower.includes('phone')) type = 'phone'; else if (nameLower.includes('watch') || nameLower.includes('buds')) type = 'watch'; else if (nameLower.includes('macbook')) type = 'laptop'; setDevices(curr => { // Randomly update existing or add new if (curr.length > 0 && Math.random() > 0.4) { const idx = Math.floor(Math.random() * curr.length); const updated = [...curr]; updated[idx].rssi = rssi; updated[idx].lastSeen = new Date(); return updated; } if (curr.length < 12) { setDebugLogs(logs => [...logs, `[${new Date().toISOString().split('T')[1].slice(0,-1)}] DEBUG: Discovered BLE MAC ${newMac} (${rssi}dBm)`]); return [...curr, { mac: newMac, name, rssi, lastSeen: new Date(), type }]; } return curr; }); } return prev - 1; }); }, 1000); } return () => clearInterval(interval); }, [isScanning, timeRemaining]); // Audio Effect useEffect(() => { if (usingRtlSdr && rtlSdrRef.current) { rtlSdrRef.current.setGain(gain * 10).catch(console.error); } }, [gain, usingRtlSdr]); useEffect(() => { if (audioEnabled && !audioCtxRef.current) { const AudioContext = window.AudioContext || (window as any).webkitAudioContext; audioCtxRef.current = new AudioContext(); oscillatorRef.current = audioCtxRef.current.createOscillator(); gainNodeRef.current = audioCtxRef.current.createGain(); oscillatorRef.current.connect(gainNodeRef.current); gainNodeRef.current.connect(audioCtxRef.current.destination); oscillatorRef.current.type = 'sine'; oscillatorRef.current.start(); } if (audioCtxRef.current && oscillatorRef.current && gainNodeRef.current) { if (audioEnabled) { if (audioCtxRef.current.state === 'suspended') { audioCtxRef.current.resume(); } // Map slider (0-100) to frequency (200Hz - 1000Hz) const freq = 200 + (sliderValue / 100) * 800; oscillatorRef.current.frequency.setValueAtTime(freq, audioCtxRef.current.currentTime); // Add some "static" modulation based on scanning state gainNodeRef.current.gain.setValueAtTime(isScanning ? 0.1 + Math.random() * 0.1 : 0.05, audioCtxRef.current.currentTime); } else { gainNodeRef.current.gain.setValueAtTime(0, audioCtxRef.current.currentTime); } } return () => { // Cleanup on unmount handled by audioEnabled state changes }; }, [audioEnabled, sliderValue, isScanning]); const toggleAudio = () => { setAudioEnabled(!audioEnabled); }; const getDeviceIcon = (type: string) => { switch (type) { case 'phone': return ; case 'watch': return ; case 'laptop': return ; default: return ; } }; return (

Frequency Tuner

{!('usb' in navigator) && ( )}
{/* Left Column: Controls */}
Band Presets
{PRESETS.map((preset) => ( ))}
Target Acquisition
{freqError && (
{freqError}
)}
Gain setGain(Number(e.target.value))} className="w-full accent-cyan-500" /> {gain}
setScanDuration(Number(e.target.value) || 30)} className="bg-zinc-800 border-zinc-700 font-mono text-xs" disabled={isScanning} />
{isScanning && ( )}
{devices.length > 0 && (
Discovered Devices ({devices.length})
{devices.map(dev => (
{getDeviceIcon(dev.type)}
{dev.mac} {dev.name}
{dev.rssi} dBm {dev.lastSeen.toLocaleTimeString()}
))}
)} {debugLogs.length > 0 && (
Firmware Debug Console {isScanning && Active}
{isScanning && (
)}
{debugLogs.map((log, i) => (
{log}
))}
)}
{/* Right Column: Fine Tuning Slider */} Fine Tune
{/* Custom vertical slider implementation */} setSliderValue(parseInt(e.target.value))} className="w-48 h-2 -rotate-90 origin-center absolute top-1/2 -translate-y-1/2 appearance-none bg-transparent cursor-pointer [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-8 [&::-webkit-slider-thumb]:h-4 [&::-webkit-slider-thumb]:bg-cyan-400 [&::-webkit-slider-thumb]:rounded-sm" />
{(sliderValue).toFixed(1)}% Offset
); }