|
|
<!DOCTYPE html> |
|
|
<html lang="en"> |
|
|
|
|
|
<head> |
|
|
<meta charset="UTF-8"> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
|
<title>ReachyMini Controller</title> |
|
|
<style> |
|
|
* { |
|
|
margin: 0; |
|
|
padding: 0; |
|
|
box-sizing: border-box; |
|
|
} |
|
|
|
|
|
body { |
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; |
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
|
|
min-height: 100vh; |
|
|
display: flex; |
|
|
justify-content: center; |
|
|
align-items: center; |
|
|
padding: 20px; |
|
|
} |
|
|
|
|
|
.container { |
|
|
background: white; |
|
|
border-radius: 20px; |
|
|
padding: 40px; |
|
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); |
|
|
max-width: 500px; |
|
|
width: 100%; |
|
|
} |
|
|
|
|
|
h1 { |
|
|
color: #333; |
|
|
margin-bottom: 10px; |
|
|
font-size: 28px; |
|
|
} |
|
|
|
|
|
.status { |
|
|
padding: 12px; |
|
|
border-radius: 8px; |
|
|
margin: 20px 0; |
|
|
font-weight: 500; |
|
|
text-align: center; |
|
|
} |
|
|
|
|
|
.status.disconnected { |
|
|
background: #fee; |
|
|
color: #c33; |
|
|
} |
|
|
|
|
|
.status.connected { |
|
|
background: #efe; |
|
|
color: #3c3; |
|
|
} |
|
|
|
|
|
.status.connecting { |
|
|
background: #ffeaa7; |
|
|
color: #d63031; |
|
|
} |
|
|
|
|
|
.connect-btn { |
|
|
width: 100%; |
|
|
padding: 15px; |
|
|
background: #667eea; |
|
|
color: white; |
|
|
border: none; |
|
|
border-radius: 10px; |
|
|
font-size: 16px; |
|
|
font-weight: 600; |
|
|
cursor: pointer; |
|
|
transition: all 0.3s; |
|
|
margin-bottom: 30px; |
|
|
} |
|
|
|
|
|
.connect-btn:hover { |
|
|
background: #5568d3; |
|
|
transform: translateY(-2px); |
|
|
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4); |
|
|
} |
|
|
|
|
|
.connect-btn:disabled { |
|
|
background: #ccc; |
|
|
cursor: not-allowed; |
|
|
transform: none; |
|
|
} |
|
|
|
|
|
.commands { |
|
|
display: grid; |
|
|
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); |
|
|
gap: 15px; |
|
|
} |
|
|
|
|
|
.command-btn { |
|
|
padding: 20px; |
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
|
|
color: white; |
|
|
border: none; |
|
|
border-radius: 10px; |
|
|
font-size: 14px; |
|
|
font-weight: 600; |
|
|
cursor: pointer; |
|
|
transition: all 0.3s; |
|
|
text-transform: uppercase; |
|
|
letter-spacing: 0.5px; |
|
|
} |
|
|
|
|
|
.command-btn:hover:not(:disabled) { |
|
|
transform: translateY(-3px); |
|
|
box-shadow: 0 8px 20px rgba(102, 126, 234, 0.4); |
|
|
} |
|
|
|
|
|
.command-btn:active:not(:disabled) { |
|
|
transform: translateY(-1px); |
|
|
} |
|
|
|
|
|
.command-btn:disabled { |
|
|
opacity: 0.5; |
|
|
cursor: not-allowed; |
|
|
transform: none; |
|
|
} |
|
|
|
|
|
.log { |
|
|
margin-top: 30px; |
|
|
padding: 15px; |
|
|
background: #f8f9fa; |
|
|
border-radius: 8px; |
|
|
max-height: 200px; |
|
|
overflow-y: auto; |
|
|
font-size: 12px; |
|
|
font-family: 'Courier New', monospace; |
|
|
} |
|
|
|
|
|
.log-entry { |
|
|
padding: 5px 0; |
|
|
border-bottom: 1px solid #e9ecef; |
|
|
} |
|
|
|
|
|
.log-entry:last-child { |
|
|
border-bottom: none; |
|
|
} |
|
|
|
|
|
.log-entry.error { |
|
|
color: #c33; |
|
|
} |
|
|
|
|
|
.log-entry.success { |
|
|
color: #3c3; |
|
|
} |
|
|
|
|
|
.note { |
|
|
margin-top: 20px; |
|
|
padding: 15px; |
|
|
background: #fff3cd; |
|
|
border-left: 4px solid #ffc107; |
|
|
border-radius: 4px; |
|
|
font-size: 14px; |
|
|
color: #856404; |
|
|
} |
|
|
</style> |
|
|
</head> |
|
|
|
|
|
<body> |
|
|
<div class="container"> |
|
|
<h1>🤖 ReachyMini Controller</h1> |
|
|
|
|
|
<div id="status" class="status disconnected"> |
|
|
Disconnected |
|
|
</div> |
|
|
|
|
|
<button id="connectBtn" class="connect-btn"> |
|
|
Connect to ReachyMini |
|
|
</button> |
|
|
|
|
|
<div class="commands"> |
|
|
<button class="command-btn" data-command="forward" disabled>Forward</button> |
|
|
<button class="command-btn" data-command="backward" disabled>Backward</button> |
|
|
<button class="command-btn" data-command="left" disabled>Turn Left</button> |
|
|
<button class="command-btn" data-command="right" disabled>Turn Right</button> |
|
|
<button class="command-btn" data-command="stop" disabled>Stop</button> |
|
|
<button class="command-btn" data-command="wave" disabled>Wave</button> |
|
|
</div> |
|
|
|
|
|
<div class="log" id="log"></div> |
|
|
|
|
|
<div class="note"> |
|
|
<strong>Note:</strong> Web Bluetooth requires HTTPS. Make sure you're accessing this page securely. Your |
|
|
browser must also support Web Bluetooth API (Chrome, Edge, Opera). |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<script> |
|
|
let device = null; |
|
|
let characteristic = null; |
|
|
const statusEl = document.getElementById('status'); |
|
|
const connectBtn = document.getElementById('connectBtn'); |
|
|
const commandBtns = document.querySelectorAll('.command-btn'); |
|
|
const logEl = document.getElementById('log'); |
|
|
|
|
|
|
|
|
if (!navigator.bluetooth) { |
|
|
updateStatus('Web Bluetooth not supported', 'disconnected'); |
|
|
addLog('ERROR: Web Bluetooth API not available in this browser', 'error'); |
|
|
connectBtn.disabled = true; |
|
|
} |
|
|
|
|
|
function addLog(message, type = '') { |
|
|
const entry = document.createElement('div'); |
|
|
entry.className = `log-entry ${type}`; |
|
|
const timestamp = new Date().toLocaleTimeString(); |
|
|
entry.textContent = `[${timestamp}] ${message}`; |
|
|
logEl.appendChild(entry); |
|
|
logEl.scrollTop = logEl.scrollHeight; |
|
|
} |
|
|
|
|
|
function updateStatus(message, state) { |
|
|
statusEl.textContent = message; |
|
|
statusEl.className = `status ${state}`; |
|
|
} |
|
|
|
|
|
async function connectToDevice() { |
|
|
try { |
|
|
updateStatus('Scanning for devices...', 'connecting'); |
|
|
addLog('Requesting Bluetooth device...'); |
|
|
|
|
|
device = await navigator.bluetooth.requestDevice({ |
|
|
filters: [{ name: 'ReachyMini' }], |
|
|
optionalServices: ['0000ffe0-0000-1000-8000-00805f9b34fb'] |
|
|
}); |
|
|
|
|
|
addLog(`Found device: ${device.name}`); |
|
|
updateStatus('Connecting...', 'connecting'); |
|
|
|
|
|
const server = await device.gatt.connect(); |
|
|
addLog('Connected to GATT server'); |
|
|
|
|
|
|
|
|
const service = await server.getPrimaryService('0000ffe0-0000-1000-8000-00805f9b34fb'); |
|
|
addLog('Got service'); |
|
|
|
|
|
|
|
|
characteristic = await service.getCharacteristic('0000ffe1-0000-1000-8000-00805f9b34fb'); |
|
|
addLog('Got characteristic'); |
|
|
|
|
|
updateStatus('Connected to ReachyMini', 'connected'); |
|
|
addLog('Successfully connected!', 'success'); |
|
|
|
|
|
connectBtn.textContent = 'Disconnect'; |
|
|
commandBtns.forEach(btn => btn.disabled = false); |
|
|
|
|
|
device.addEventListener('gattserverdisconnected', onDisconnected); |
|
|
|
|
|
} catch (error) { |
|
|
addLog(`Connection failed: ${error.message}`, 'error'); |
|
|
updateStatus('Connection failed', 'disconnected'); |
|
|
console.error('Connection error:', error); |
|
|
} |
|
|
} |
|
|
|
|
|
function onDisconnected() { |
|
|
updateStatus('Disconnected', 'disconnected'); |
|
|
addLog('Device disconnected', 'error'); |
|
|
connectBtn.textContent = 'Connect to ReachyMini'; |
|
|
commandBtns.forEach(btn => btn.disabled = true); |
|
|
characteristic = null; |
|
|
device = null; |
|
|
} |
|
|
|
|
|
async function disconnect() { |
|
|
if (device && device.gatt.connected) { |
|
|
device.gatt.disconnect(); |
|
|
addLog('Manually disconnected'); |
|
|
} |
|
|
} |
|
|
|
|
|
async function sendCommand(command) { |
|
|
if (!characteristic) { |
|
|
addLog('Not connected to device', 'error'); |
|
|
return; |
|
|
} |
|
|
|
|
|
try { |
|
|
const encoder = new TextEncoder(); |
|
|
const data = encoder.encode(command); |
|
|
await characteristic.writeValue(data); |
|
|
addLog(`Sent command: ${command}`, 'success'); |
|
|
} catch (error) { |
|
|
addLog(`Failed to send command: ${error.message}`, 'error'); |
|
|
console.error('Send error:', error); |
|
|
} |
|
|
} |
|
|
|
|
|
connectBtn.addEventListener('click', async () => { |
|
|
if (device && device.gatt.connected) { |
|
|
await disconnect(); |
|
|
} else { |
|
|
await connectToDevice(); |
|
|
} |
|
|
}); |
|
|
|
|
|
commandBtns.forEach(btn => { |
|
|
btn.addEventListener('click', () => { |
|
|
const command = btn.dataset.command; |
|
|
sendCommand(command); |
|
|
}); |
|
|
}); |
|
|
|
|
|
addLog('Ready. Click "Connect to ReachyMini" to start.'); |
|
|
</script> |
|
|
</body> |
|
|
|
|
|
</html> |