bluetooth_panel.rs
1 use dioxus::prelude::*; 2 use ui::components::button::{Button, ButtonVariant}; 3 4 #[component] 5 pub fn BluetoothPanel() -> Element { 6 let mut initialized = use_signal(|| false); 7 8 use_effect(move || { 9 if *initialized.peek() { 10 return; 11 } 12 initialized.set(true); 13 14 document::eval( 15 r#" 16 setTimeout(function() { 17 const panel = document.getElementById('bluetooth-panel'); 18 if (!panel || panel.dataset.initialized) return; 19 panel.dataset.initialized = 'true'; 20 21 const pairBtn = document.getElementById('bt-pair-btn'); 22 const disconnectBtn = document.getElementById('bt-disconnect-btn'); 23 const statusEl = document.getElementById('bt-status'); 24 const termEl = document.getElementById('bt-terminal'); 25 26 let device = null; 27 let server = null; 28 let rxChar = null; 29 let txChar = null; 30 let term = null; 31 32 const NUS_SERVICE = '6e400001-b5a3-f393-e0a9-e50e24dcca9e'; 33 const NUS_RX = '6e400002-b5a3-f393-e0a9-e50e24dcca9e'; 34 const NUS_TX = '6e400003-b5a3-f393-e0a9-e50e24dcca9e'; 35 36 function setStatus(text) { statusEl.textContent = text; } 37 38 function showConnected() { 39 pairBtn.style.display = 'none'; 40 disconnectBtn.style.display = ''; 41 termEl.style.display = ''; 42 } 43 44 function showDisconnected() { 45 pairBtn.style.display = ''; 46 disconnectBtn.style.display = 'none'; 47 } 48 49 function initTerminal() { 50 if (term) return; 51 if (typeof Terminal === 'undefined') return; 52 term = new Terminal({ 53 cursorBlink: true, fontSize: 13, 54 fontFamily: "'JetBrains Mono', 'Fira Code', monospace", 55 theme: { 56 background: '#0a0a0c', foreground: '#d4a84b', cursor: '#f5b72b', 57 selectionBackground: 'rgba(245, 183, 43, 0.3)', 58 black: '#0a0a0c', red: '#e06c6c', green: '#6cc070', 59 yellow: '#f5b72b', blue: '#6c9ee0', magenta: '#c06cc0', 60 cyan: '#6cc0c0', white: '#d4a84b' 61 } 62 }); 63 if (typeof FitAddon !== 'undefined') { 64 const fit = new FitAddon.FitAddon(); 65 term.loadAddon(fit); 66 term.open(termEl); 67 fit.fit(); 68 window.addEventListener('resize', () => fit.fit()); 69 } else { 70 term.open(termEl); 71 } 72 } 73 74 pairBtn.addEventListener('click', async function() { 75 if (!navigator.bluetooth) { 76 setStatus('Web Bluetooth not supported. Use Chrome/Edge on HTTPS.'); 77 return; 78 } 79 try { 80 setStatus('Scanning...'); 81 device = await navigator.bluetooth.requestDevice({ 82 filters: [{ services: [NUS_SERVICE] }], 83 optionalServices: [NUS_SERVICE] 84 }); 85 86 device.addEventListener('gattserverdisconnected', function() { 87 setStatus('Disconnected'); 88 showDisconnected(); 89 if (term) term.write('\r\n\x1b[31m[BLE disconnected]\x1b[0m\r\n'); 90 }); 91 92 setStatus('Connecting to ' + device.name + '...'); 93 server = await device.gatt.connect(); 94 const service = await server.getPrimaryService(NUS_SERVICE); 95 96 rxChar = await service.getCharacteristic(NUS_RX); 97 txChar = await service.getCharacteristic(NUS_TX); 98 99 await txChar.startNotifications(); 100 txChar.addEventListener('characteristicvaluechanged', function(event) { 101 const decoder = new TextDecoder(); 102 const text = decoder.decode(event.target.value); 103 if (term) term.write(text); 104 }); 105 106 initTerminal(); 107 showConnected(); 108 setStatus('Connected: ' + (device.name || 'BLE Device')); 109 if (term) term.write('\x1b[32m[BLE connected: ' + device.name + ']\x1b[0m\r\n'); 110 111 term.onData(function(data) { 112 if (rxChar) { 113 const encoder = new TextEncoder(); 114 rxChar.writeValueWithoutResponse(encoder.encode(data)); 115 } 116 }); 117 118 } catch (err) { 119 setStatus('Error: ' + err.message); 120 } 121 }); 122 123 disconnectBtn.addEventListener('click', function() { 124 if (device && device.gatt.connected) { 125 device.gatt.disconnect(); 126 } 127 showDisconnected(); 128 setStatus('Disconnected'); 129 }); 130 131 showDisconnected(); 132 setStatus('Ready to pair'); 133 }, 100); 134 "#, 135 ); 136 }); 137 138 rsx! { 139 section { id: "bluetooth-panel", class: "border border-border rounded-lg bg-card p-4", 140 div { class: "flex items-center gap-2 mb-3", 141 h2 { class: "text-xl font-semibold", "Bluetooth" } 142 span { id: "bt-status", class: "text-sm text-muted-foreground", role: "status", aria_live: "polite" } 143 } 144 145 div { class: "flex gap-2 mb-3", 146 Button { id: "bt-pair-btn", variant: ButtonVariant::Outline, 147 class: "px-3 py-1.5 text-sm hover:bg-muted/50".to_string(), 148 aria_label: "Pair via Bluetooth", 149 lucide_dioxus::Bluetooth { class: "w-4 h-4" } 150 "Pair via Bluetooth" 151 } 152 Button { id: "bt-disconnect-btn", variant: ButtonVariant::Outline, 153 class: "px-3 py-1.5 text-sm text-muted-foreground hover:bg-muted/50".to_string(), style: "display:none", 154 aria_label: "Disconnect Bluetooth", 155 "Disconnect" 156 } 157 } 158 159 div { id: "bt-terminal", class: "h-[250px] bg-[#0a0a0c] rounded-lg overflow-hidden", style: "display:none" } 160 } 161 } 162 }