Initial
This commit is contained in:
170
main.html
Normal file
170
main.html
Normal file
@@ -0,0 +1,170 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Pybricks Hub</title>
|
||||
<style>
|
||||
body {
|
||||
background: #0a0a0f;
|
||||
color: #e0e0f0;
|
||||
font-family: monospace;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
button {
|
||||
background: #e8ff47;
|
||||
color: #0a0a0f;
|
||||
border: none;
|
||||
padding: 8px 18px;
|
||||
font-family: monospace;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
#log {
|
||||
margin-top: 16px;
|
||||
background: #111118;
|
||||
padding: 16px;
|
||||
min-height: 200px;
|
||||
white-space: pre-wrap;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.status {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: #555570;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<button id="connectBtn" onclick="connect()">Connect</button>
|
||||
<button id="uploadBtn" onclick="upload()" disabled>Upload main.mpy</button>
|
||||
<button id="disconnectBtn" onclick="disconnect()" disabled>Disconnect</button>
|
||||
<div class="status" id="status">Disconnected</div>
|
||||
<div id="log"></div>
|
||||
|
||||
<script>
|
||||
const SERVICE_UUID = 'c5f50001-8280-46da-89f4-6d8051e4aeef';
|
||||
const CMD_EVENT_UUID = 'c5f50002-8280-46da-89f4-6d8051e4aeef';
|
||||
const CAPABILITIES_UUID = 'c5f50003-8280-46da-89f4-6d8051e4aeef';
|
||||
|
||||
// Command codes
|
||||
const CMD_WRITE_USER_PROGRAM_META = 0x06;
|
||||
const CMD_WRITE_USER_RAM = 0x03;
|
||||
|
||||
let device, server, cmdChar, maxDataSize = 20;
|
||||
|
||||
function log(msg) {
|
||||
document.getElementById('log').textContent += msg + '\n';
|
||||
}
|
||||
|
||||
function setStatus(msg) {
|
||||
document.getElementById('status').textContent = msg;
|
||||
}
|
||||
|
||||
function onNotify(event) {
|
||||
const data = new Uint8Array(event.target.value.buffer);
|
||||
if (data[0] === 0x01) {
|
||||
// stdout event
|
||||
const text = new TextDecoder().decode(data.slice(1));
|
||||
log(text);
|
||||
}
|
||||
}
|
||||
|
||||
async function connect() {
|
||||
try {
|
||||
setStatus('Scanning...');
|
||||
device = await navigator.bluetooth.requestDevice({
|
||||
filters: [{ services: [SERVICE_UUID] }],
|
||||
optionalServices: [SERVICE_UUID]
|
||||
});
|
||||
|
||||
device.addEventListener('gattserverdisconnected', () => {
|
||||
setStatus('Disconnected');
|
||||
document.getElementById('connectBtn').disabled = false;
|
||||
document.getElementById('uploadBtn').disabled = true;
|
||||
document.getElementById('disconnectBtn').disabled = true;
|
||||
});
|
||||
|
||||
setStatus('Connecting...');
|
||||
server = await device.gatt.connect();
|
||||
const service = await server.getPrimaryService(SERVICE_UUID);
|
||||
cmdChar = await service.getCharacteristic(CMD_EVENT_UUID);
|
||||
|
||||
// Read capabilities to get max data size
|
||||
try {
|
||||
const capChar = await service.getCharacteristic(CAPABILITIES_UUID);
|
||||
const caps = new DataView((await capChar.readValue()).buffer);
|
||||
maxDataSize = caps.getUint16(0, true); // bytes 0-1: max write size
|
||||
log(`Hub max data size: ${maxDataSize} bytes`);
|
||||
} catch (e) {
|
||||
log('Could not read capabilities, using default chunk size of 20 bytes');
|
||||
}
|
||||
|
||||
await cmdChar.startNotifications();
|
||||
cmdChar.addEventListener('characteristicvaluechanged', onNotify);
|
||||
|
||||
setStatus(`Connected to ${device.name}`);
|
||||
log(`Connected to ${device.name}`);
|
||||
document.getElementById('connectBtn').disabled = true;
|
||||
document.getElementById('uploadBtn').disabled = false;
|
||||
document.getElementById('disconnectBtn').disabled = false;
|
||||
} catch (e) {
|
||||
setStatus('Connection failed');
|
||||
log('Error: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function upload() {
|
||||
try {
|
||||
const resp = await fetch('./main.mpy');
|
||||
if (!resp.ok) throw new Error('Could not fetch main.mpy');
|
||||
const mpy = new Uint8Array(await resp.arrayBuffer());
|
||||
|
||||
log(`Uploading ${mpy.length} bytes...`);
|
||||
setStatus('Uploading...');
|
||||
|
||||
// Write program metadata: cmd(1) + size(4 LE)
|
||||
const meta = new Uint8Array(5);
|
||||
meta[0] = CMD_WRITE_USER_PROGRAM_META;
|
||||
new DataView(meta.buffer).setUint32(1, mpy.length, true);
|
||||
await cmdChar.writeValueWithResponse(meta);
|
||||
|
||||
// Write program data in chunks: cmd(1) + offset(4 LE) + data
|
||||
const chunkSize = maxDataSize - 5; // 1 byte cmd + 4 bytes offset
|
||||
for (let offset = 0; offset < mpy.length; offset += chunkSize) {
|
||||
const chunk = mpy.slice(offset, offset + chunkSize);
|
||||
const packet = new Uint8Array(5 + chunk.length);
|
||||
packet[0] = CMD_WRITE_USER_RAM;
|
||||
new DataView(packet.buffer).setUint32(1, offset, true);
|
||||
packet.set(chunk, 5);
|
||||
await cmdChar.writeValueWithResponse(packet);
|
||||
}
|
||||
|
||||
setStatus('Upload complete');
|
||||
log('Upload complete. Press the hub button to run.');
|
||||
} catch (e) {
|
||||
setStatus('Upload failed');
|
||||
log('Error: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function disconnect() {
|
||||
if (device && device.gatt.connected) {
|
||||
device.gatt.disconnect();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Reference in New Issue
Block a user