Skip to content

Instantly share code, notes, and snippets.

@Yossi
Created November 28, 2025 03:11
Show Gist options
  • Select an option

  • Save Yossi/eb7c622311824552404a4f877424f050 to your computer and use it in GitHub Desktop.

Select an option

Save Yossi/eb7c622311824552404a4f877424f050 to your computer and use it in GitHub Desktop.
<!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