Created
November 28, 2025 03:11
-
-
Save Yossi/eb7c622311824552404a4f877424f050 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html> | |
| <head> | |
| <title>Pico LED BLE Control</title> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <style> | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| min-height: 100vh; | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| padding: 20px; | |
| } | |
| .container { | |
| background: white; | |
| border-radius: 20px; | |
| padding: 40px; | |
| box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); | |
| max-width: 400px; | |
| width: 100%; | |
| text-align: center; | |
| } | |
| h1 { | |
| color: #333; | |
| margin-bottom: 10px; | |
| font-size: 28px; | |
| } | |
| .subtitle { | |
| color: #666; | |
| margin-bottom: 30px; | |
| font-size: 14px; | |
| } | |
| button { | |
| width: 100%; | |
| padding: 15px; | |
| margin: 10px 0; | |
| border: none; | |
| border-radius: 10px; | |
| font-size: 16px; | |
| font-weight: bold; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| text-transform: uppercase; | |
| letter-spacing: 1px; | |
| } | |
| button:disabled { | |
| opacity: 0.5; | |
| cursor: not-allowed; | |
| } | |
| .connect-btn { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| color: white; | |
| } | |
| .connect-btn:hover:not(:disabled) { | |
| transform: translateY(-2px); | |
| box-shadow: 0 5px 20px rgba(102, 126, 234, 0.4); | |
| } | |
| .connect-btn:active { | |
| transform: translateY(0); | |
| } | |
| .on-btn { | |
| background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); | |
| color: white; | |
| } | |
| .on-btn:hover:not(:disabled) { | |
| transform: translateY(-2px); | |
| box-shadow: 0 5px 20px rgba(245, 87, 108, 0.4); | |
| } | |
| .off-btn { | |
| background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); | |
| color: white; | |
| } | |
| .off-btn:hover:not(:disabled) { | |
| transform: translateY(-2px); | |
| box-shadow: 0 5px 20px rgba(79, 172, 254, 0.4); | |
| } | |
| .toggle-btn { | |
| background: linear-gradient(135deg, #fa709a 0%, #fee140 100%); | |
| color: white; | |
| } | |
| .toggle-btn:hover:not(:disabled) { | |
| transform: translateY(-2px); | |
| box-shadow: 0 5px 20px rgba(250, 112, 154, 0.4); | |
| } | |
| .status { | |
| margin: 20px 0; | |
| padding: 15px; | |
| border-radius: 10px; | |
| font-size: 14px; | |
| font-weight: 500; | |
| } | |
| .status.disconnected { | |
| background: #fee; | |
| color: #c33; | |
| border: 2px solid #fcc; | |
| } | |
| .status.connecting { | |
| background: #ffe; | |
| color: #963; | |
| border: 2px solid #ffc; | |
| } | |
| .status.connected { | |
| background: #efe; | |
| color: #363; | |
| border: 2px solid #cfc; | |
| } | |
| .led-indicator { | |
| width: 80px; | |
| height: 80px; | |
| border-radius: 50%; | |
| margin: 20px auto; | |
| border: 4px solid #ddd; | |
| transition: all 0.3s ease; | |
| box-shadow: inset 0 0 20px rgba(0, 0, 0, 0.1); | |
| } | |
| .led-indicator.on { | |
| background: radial-gradient(circle, #ffeb3b 0%, #ffc107 100%); | |
| border-color: #ff9800; | |
| box-shadow: | |
| inset 0 0 20px rgba(255, 255, 255, 0.5), | |
| 0 0 30px rgba(255, 235, 59, 0.6), | |
| 0 0 60px rgba(255, 235, 59, 0.4); | |
| } | |
| .led-indicator.off { | |
| background: #555; | |
| border-color: #333; | |
| } | |
| .controls { | |
| display: none; | |
| } | |
| .controls.active { | |
| display: block; | |
| } | |
| .error { | |
| background: #fee; | |
| color: #c33; | |
| padding: 15px; | |
| border-radius: 10px; | |
| margin: 20px 0; | |
| border: 2px solid #fcc; | |
| display: none; | |
| font-size: 13px; | |
| line-height: 1.6; | |
| } | |
| .error.show { | |
| display: block; | |
| } | |
| .info { | |
| background: #e3f2fd; | |
| color: #1565c0; | |
| padding: 15px; | |
| border-radius: 10px; | |
| margin: 20px 0; | |
| border: 2px solid #90caf9; | |
| font-size: 13px; | |
| line-height: 1.6; | |
| } | |
| @keyframes pulse { | |
| 0%, 100% { opacity: 1; } | |
| 50% { opacity: 0.5; } | |
| } | |
| .connecting-indicator { | |
| animation: pulse 1.5s ease-in-out infinite; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <h1>Pico LED Control</h1> | |
| <p class="subtitle">Bluetooth Low Energy Demo</p> | |
| <button id="connectBtn" class="connect-btn" onclick="connect()"> | |
| Connect to Pico LED | |
| </button> | |
| <div id="status" class="status disconnected"> | |
| Not Connected | |
| </div> | |
| <div id="error" class="error"></div> | |
| <div id="info" class="info" style="display: none;"></div> | |
| <div id="controls" class="controls"> | |
| <div id="ledIndicator" class="led-indicator off"></div> | |
| <button class="on-btn" onclick="turnOn()"> | |
| Turn ON | |
| </button> | |
| <button class="off-btn" onclick="turnOff()"> | |
| Turn OFF | |
| </button> | |
| <button class="toggle-btn" onclick="toggle()"> | |
| Toggle | |
| </button> | |
| </div> | |
| </div> | |
| <script> | |
| const LED_SERVICE_UUID = 0x1815; | |
| const LED_CHAR_UUID = 0x2A56; | |
| let device; | |
| let characteristic; | |
| let ledState = false; | |
| // Detailed Web Bluetooth detection | |
| function checkWebBluetooth() { | |
| const errorDiv = document.getElementById('error'); | |
| const infoDiv = document.getElementById('info'); | |
| const connectBtn = document.getElementById('connectBtn'); | |
| // Check if running on HTTPS or localhost | |
| const isSecure = window.location.protocol === 'https:' || | |
| window.location.hostname === 'localhost' || | |
| window.location.hostname === '127.0.0.1' || | |
| window.location.protocol === 'file:'; | |
| if (!navigator.bluetooth) { | |
| let errorMsg = '<strong>Web Bluetooth not available.</strong><br><br>'; | |
| // Detect browser | |
| const ua = navigator.userAgent; | |
| const isChrome = /Chrome/.test(ua) && /Google Inc/.test(navigator.vendor); | |
| const isEdge = /Edg/.test(ua); | |
| const isAndroid = /Android/.test(ua); | |
| if (!isAndroid) { | |
| errorMsg += 'You need an <strong>Android</strong> device.<br>'; | |
| } else if (!isChrome && !isEdge) { | |
| errorMsg += 'You need <strong>Chrome</strong> or <strong>Edge</strong> browser.<br>'; | |
| } else if (!isSecure) { | |
| errorMsg += 'Page must be served over <strong>HTTPS</strong> or <strong>localhost</strong>.<br><br>'; | |
| errorMsg += '<strong>Solutions:</strong><br>'; | |
| errorMsg += '1. Use GitHub Pages (auto HTTPS)<br>'; | |
| errorMsg += '2. Use a local server with ngrok<br>'; | |
| errorMsg += '3. Enable chrome://flags/#unsafely-treat-insecure-origin-as-secure'; | |
| infoDiv.innerHTML = '<strong>Quick Fix:</strong><br>In Chrome, go to:<br><code>chrome://flags/#unsafely-treat-insecure-origin-as-secure</code><br>Add this page\'s URL and restart Chrome.'; | |
| infoDiv.style.display = 'block'; | |
| } else { | |
| errorMsg += 'Try enabling Web Bluetooth in:<br>'; | |
| errorMsg += '<code>chrome://flags/#enable-web-bluetooth</code>'; | |
| } | |
| errorDiv.innerHTML = errorMsg; | |
| errorDiv.classList.add('show'); | |
| connectBtn.disabled = true; | |
| return false; | |
| } | |
| return true; | |
| } | |
| // Run check on load | |
| const bluetoothAvailable = checkWebBluetooth(); | |
| async function connect() { | |
| try { | |
| updateStatus('Scanning for devices...', 'connecting'); | |
| document.getElementById('connectBtn').disabled = true; | |
| // Request device | |
| device = await navigator.bluetooth.requestDevice({ | |
| filters: [{ name: 'Pico LED' }], | |
| optionalServices: [LED_SERVICE_UUID] | |
| }); | |
| updateStatus('Connecting...', 'connecting'); | |
| // Connect to GATT server | |
| const server = await device.gatt.connect(); | |
| // Get service | |
| const service = await server.getPrimaryService(LED_SERVICE_UUID); | |
| // Get characteristic | |
| characteristic = await service.getCharacteristic(LED_CHAR_UUID); | |
| // Subscribe to notifications | |
| await characteristic.startNotifications(); | |
| characteristic.addEventListener('characteristicvaluechanged', handleNotification); | |
| // Read initial state | |
| const value = await characteristic.readValue(); | |
| ledState = value.getUint8(0) === 1; | |
| updateLED(); | |
| updateStatus('✓ Connected!', 'connected'); | |
| document.getElementById('controls').classList.add('active'); | |
| // Handle disconnection | |
| device.addEventListener('gattserverdisconnected', onDisconnected); | |
| } catch (error) { | |
| console.error('Connection error:', error); | |
| updateStatus('Connection failed: ' + error.message, 'disconnected'); | |
| document.getElementById('connectBtn').disabled = false; | |
| } | |
| } | |
| function onDisconnected() { | |
| updateStatus('Disconnected', 'disconnected'); | |
| document.getElementById('controls').classList.remove('active'); | |
| document.getElementById('connectBtn').disabled = false; | |
| document.getElementById('connectBtn').textContent = 'Reconnect'; | |
| } | |
| function handleNotification(event) { | |
| const value = event.target.value; | |
| ledState = value.getUint8(0) === 1; | |
| updateLED(); | |
| console.log('LED state updated:', ledState ? 'ON' : 'OFF'); | |
| } | |
| async function turnOn() { | |
| await writeValue(1); | |
| } | |
| async function turnOff() { | |
| await writeValue(0); | |
| } | |
| async function toggle() { | |
| await writeValue(ledState ? 0 : 1); | |
| } | |
| async function writeValue(value) { | |
| if (!characteristic) { | |
| alert('Not connected!'); | |
| return; | |
| } | |
| try { | |
| const data = new Uint8Array([value]); | |
| await characteristic.writeValue(data); | |
| console.log('Wrote value:', value); | |
| } catch (error) { | |
| console.error('Write error:', error); | |
| alert('Failed to write: ' + error.message); | |
| } | |
| } | |
| function updateLED() { | |
| const indicator = document.getElementById('ledIndicator'); | |
| if (ledState) { | |
| indicator.classList.remove('off'); | |
| indicator.classList.add('on'); | |
| } else { | |
| indicator.classList.remove('on'); | |
| indicator.classList.add('off'); | |
| } | |
| } | |
| function updateStatus(message, type) { | |
| const statusDiv = document.getElementById('status'); | |
| statusDiv.textContent = message; | |
| statusDiv.className = 'status ' + type; | |
| if (type === 'connecting') { | |
| statusDiv.classList.add('connecting-indicator'); | |
| } else { | |
| statusDiv.classList.remove('connecting-indicator'); | |
| } | |
| } | |
| // Log for debugging | |
| console.log('BLE LED Controller loaded'); | |
| console.log('Web Bluetooth supported:', !!navigator.bluetooth); | |
| console.log('Protocol:', window.location.protocol); | |
| console.log('Hostname:', window.location.hostname); | |
| console.log('User Agent:', navigator.userAgent); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment