Working loader

This commit is contained in:
2026-03-21 16:34:44 -05:00
parent 08997c7109
commit 80097b7231
5 changed files with 233 additions and 85 deletions

BIN
main.bin Normal file

Binary file not shown.

310
main.html
View File

@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<title>Pybricks Hub</title>
<title>Pybricks Hub Loader</title>
<style>
body {
background: #0a0a0f;
@@ -21,6 +21,7 @@
font-weight: bold;
cursor: pointer;
margin-right: 8px;
margin-bottom: 8px;
}
button:disabled {
@@ -36,50 +37,138 @@
white-space: pre-wrap;
font-size: 13px;
line-height: 1.6;
border: 1px solid #222;
}
.status {
margin-top: 8px;
font-size: 12px;
color: #555570;
color: #888;
}
</style>
</head>
<body>
<button id="connectBtn" onclick="connect()">Connect</button>
<button id="uploadBtn" onclick="upload()" disabled>Upload main.mpy</button>
<button id="uploadBtn" onclick="upload()" disabled>Upload & Run</button>
<button id="stopBtn" onclick="stopProgram()" disabled>Stop</button>
<button id="disconnectBtn" onclick="disconnect()" disabled>Disconnect</button>
<button onclick="document.getElementById('log').textContent=''">Clear</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';
// UUIDs for Pybricks service / characteristics (fixed by firmware)
const PYBRICKS_SERVICE_UUID = 'c5f50001-8280-46da-89f4-6d8051e4aeef';
const PYBRICKS_CMD_EVENT_UUID = 'c5f50002-8280-46da-89f4-6d8051e4aeef';
const PYBRICKS_CAPABILITIES_UUID = 'c5f50003-8280-46da-89f4-6d8051e4aeef';
// Command codes
const CMD_WRITE_USER_PROGRAM_META = 0x06; // ✅ correct
const CMD_WRITE_USER_PROGRAM = 0x07; // writes to FLASH (button runnable)
const CMD_WRITE_USER_RAM = 0x03; // writes to RAM only
const CMD_START_USER_PROGRAM = 0x01; // triggers run immediately
// Device Information Service UUIDs
const DI_SERVICE_UUID = '0000180a-0000-1000-8000-00805f9b34fb';
const SW_REV_UUID = '00002a28-0000-1000-8000-00805f9b34fb';
const FW_REV_UUID = '00002a26-0000-1000-8000-00805f9b34fb';
let device, server, cmdChar, maxDataSize = 20;
// Commands (PBIO_PYBRICKS_COMMAND_*)
const CMD_STOP_USER_PROGRAM = 0;
const CMD_START_USER_PROGRAM = 1;
const CMD_WRITE_USER_PROGRAM_META = 3; // not used here
const CMD_WRITE_USER_RAM = 4;
// Events (PBIO_PYBRICKS_EVENT_*)
const EVT_STATUS_REPORT = 0;
const EVT_WRITE_STDOUT = 1;
let device, server, cmdChar;
let maxCharSize = 20;
let lastStatusFlags = 0;
function log(msg) {
document.getElementById('log').textContent += msg + '\n';
const el = document.getElementById('log');
el.textContent += msg + '\n';
el.scrollTop = el.scrollHeight;
}
function setStatus(msg) {
document.getElementById('status').textContent = msg;
}
function sleep(ms) {
return new Promise(r => setTimeout(r, ms));
}
function u32le(n) {
const b = new Uint8Array(4);
new DataView(b.buffer).setUint32(0, n, true);
return b;
}
function concat(...arrays) {
const total = arrays.reduce((s, a) => s + a.length, 0);
const out = new Uint8Array(total);
let off = 0;
for (const a of arrays) {
out.set(a, off);
off += a.length;
}
return out;
}
function hexpreview(data, n = 12) {
return Array.from(data.slice(0, n))
.map(b => b.toString(16).padStart(2, '0')).join(' ') +
(data.length > n ? ' ...' : '');
}
async function writeCmd(data, label = '') {
log(` TX${label ? ' [' + label + ']' : ''} ${data.length}b: ${hexpreview(data)}`);
try {
await cmdChar.writeValueWithResponse(data);
} catch (e) {
log(` TX error (${label}): name=${e.name} message=${e.message}`);
if (e.name === 'NetworkError' || e.name === 'NotSupportedError') {
await sleep(200);
log(' Retrying once...');
await cmdChar.writeValueWithResponse(data);
} else {
throw e;
}
}
}
function decodeStatus(flags) {
lastStatusFlags = flags;
const bits = {
battLow: !!(flags & (1 << 0)),
battCrit: !!(flags & (1 << 1)),
running: !!(flags & (1 << 6)),
shutdown: !!(flags & (1 << 7)),
btn: !!(flags & (1 << 5)),
hostConn: !!(flags & (1 << 9)),
fileIO: !!(flags & (1 << 13)),
};
return `running=${bits.running} btn=${bits.btn} hostConn=${bits.hostConn} fileIO=${bits.fileIO} shutdown=${bits.shutdown}`;
}
function isBusy() {
const running = !!(lastStatusFlags & (1 << 6));
const fileIO = !!(lastStatusFlags & (1 << 13));
return running || fileIO;
}
function onNotify(event) {
const data = new Uint8Array(event.target.value.buffer);
if (data[0] === 0x01) {
// stdout event
if (data.length === 0) {
log('[event] empty notification');
return;
}
const type = data[0];
if (type === EVT_WRITE_STDOUT) {
const text = new TextDecoder().decode(data.slice(1));
log(text);
log('[stdout] ' + text);
} else if (type === EVT_STATUS_REPORT) {
const view = new DataView(data.buffer);
const flags = view.getUint32(1, true);
log(`[status] 0x${flags.toString(16).padStart(8, '0')} | ${decodeStatus(flags)}`);
} else {
log(`[event 0x${type.toString(16)}] ${hexpreview(data)}`);
}
}
@@ -87,108 +176,163 @@
try {
setStatus('Scanning...');
device = await navigator.bluetooth.requestDevice({
filters: [{ services: [SERVICE_UUID] }],
optionalServices: [SERVICE_UUID]
filters: [{ services: [PYBRICKS_SERVICE_UUID] }],
optionalServices: [
PYBRICKS_SERVICE_UUID,
PYBRICKS_CMD_EVENT_UUID,
PYBRICKS_CAPABILITIES_UUID,
DI_SERVICE_UUID,
]
});
device.addEventListener('gattserverdisconnected', () => {
setStatus('Disconnected');
document.getElementById('connectBtn').disabled = false;
document.getElementById('uploadBtn').disabled = true;
document.getElementById('disconnectBtn').disabled = true;
});
device.addEventListener('gattserverdisconnected', onDisconnect);
setStatus('Connecting...');
setStatus('Connecting GATT...');
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
// Device Information
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`);
const di = await server.getPrimaryService(DI_SERVICE_UUID);
const sw = new TextDecoder().decode(
await (await di.getCharacteristic(SW_REV_UUID)).readValue()
);
const fw = new TextDecoder().decode(
await (await di.getCharacteristic(FW_REV_UUID)).readValue()
);
log(`Protocol version : ${sw}`);
log(`Firmware version : ${fw}`);
} catch (e) {
log('Could not read capabilities, using default chunk size of 20 bytes');
log(`DI service: ${e.message}`);
}
const svc = await server.getPrimaryService(PYBRICKS_SERVICE_UUID);
// Capabilities
try {
const capChar = await svc.getCharacteristic(PYBRICKS_CAPABILITIES_UUID);
const raw = await capChar.readValue();
const caps = new DataView(raw.buffer);
maxCharSize = caps.getUint16(0, true);
const features = caps.getUint32(2, true);
const maxProg = caps.getUint32(6, true);
const numSlots = raw.byteLength >= 11 ? caps.getUint8(10) : 0;
log(`Max char write : ${maxCharSize} bytes`);
log(`Feature flags : 0x${features.toString(16)}`);
log(`Max program size : ${maxProg} bytes`);
log(`Slots : ${numSlots}`);
log(`MPY v6: ${!!(features & 2)} | v6.1: ${!!(features & 4)} | v6.3: ${!!(features & 32)}`);
} catch (e) {
log(`Capabilities: ${e.message}`);
}
cmdChar = await svc.getCharacteristic(PYBRICKS_CMD_EVENT_UUID);
log(`Char props: write=${cmdChar.properties.write} writeWOR=${cmdChar.properties.writeWithoutResponse} notify=${cmdChar.properties.notify}`);
await cmdChar.startNotifications();
cmdChar.addEventListener('characteristicvaluechanged', onNotify);
setStatus(`Connected to ${device.name}`);
await sleep(500); // wait for first status
log(`Connected to ${device.name}`);
setStatus(`Connected: ${device.name}`);
document.getElementById('connectBtn').disabled = true;
document.getElementById('uploadBtn').disabled = false;
document.getElementById('stopBtn').disabled = false;
document.getElementById('disconnectBtn').disabled = false;
} catch (e) {
setStatus('Connection failed');
log('Error: ' + e.message);
log(`Error: ${e.name} ${e.message}`);
console.error(e);
}
}
function crc32(buf) {
let crc = 0xFFFFFFFF;
for (let i = 0; i < buf.length; i++) {
crc ^= buf[i];
for (let j = 0; j < 8; j++) {
crc = (crc >>> 1) ^ (crc & 1 ? 0xEDB88320 : 0);
}
}
return (crc ^ 0xFFFFFFFF) >>> 0;
function onDisconnect() {
setStatus('Disconnected');
log('--- Hub disconnected ---');
document.getElementById('connectBtn').disabled = false;
document.getElementById('uploadBtn').disabled = true;
document.getElementById('stopBtn').disabled = true;
document.getElementById('disconnectBtn').disabled = true;
}
async function upload() {
if (!cmdChar) {
log('Not connected to hub.');
return;
}
// Always stop any running program first
log('Stopping any running program...');
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());
await writeCmd(new Uint8Array([CMD_STOP_USER_PROGRAM]), 'STOP');
await sleep(200);
} catch (e) {
log('Stop command ignored (no program running)');
}
/*if (isBusy()) {
log('Hub is busy (program running or file IO), not uploading.');
return;
}*/
log(`Uploading ${mpy.length} bytes...`);
setStatus('Uploading...');
document.getElementById('uploadBtn').disabled = true;
try {
const resp = await fetch('./main.bin');
if (!resp.ok) {
throw new Error(`fetch main.bin: ${resp.status} ${resp.statusText}`);
}
const blob = new Uint8Array(await resp.arrayBuffer());
log(`\nLoaded main.bin: ${blob.length} bytes`);
log(`Preview : ${hexpreview(blob, 20)}`);
// 1. Metadata
const meta = new Uint8Array(5);
meta[0] = 0x06; // CMD_WRITE_USER_PROGRAM_META
new DataView(meta.buffer).setUint32(1, mpy.length, true);
await cmdChar.writeValueWithResponse(meta);
// 2. Flash chunks
const chunkSize = maxDataSize - 5;
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] = 0x07; // CMD_WRITE_USER_PROGRAM (flash)
new DataView(packet.buffer).setUint32(1, offset, true);
packet.set(chunk, 5);
await cmdChar.writeValueWithResponse(packet);
const maxProgSize = 261512;
if (blob.length > maxProgSize) {
throw new Error(`Program too large for hub (max ${maxProgSize} bytes)`);
}
// 3. Checksum
const checksum = crc32(mpy);
const cksPacket = new Uint8Array(5);
cksPacket[0] = 0x08; // CMD_WRITE_USER_PROGRAM_CHECKSUM
new DataView(cksPacket.buffer).setUint32(1, checksum, true);
await cmdChar.writeValueWithResponse(cksPacket);
log(`Checksum: 0x${checksum.toString(16)}`);
setStatus('Uploading...');
const PAYLOAD = maxCharSize - 5; // 1 cmd + 4 offset
const chunks = Math.ceil(blob.length / PAYLOAD);
log(`\n── Upload ${blob.length} bytes in ${chunks} chunks ──`);
for (let offset = 0; offset < blob.length; offset += PAYLOAD) {
const chunk = blob.slice(offset, Math.min(offset + PAYLOAD, blob.length));
const packet = concat(
new Uint8Array([CMD_WRITE_USER_RAM]),
u32le(offset),
chunk
);
await writeCmd(packet, `RAM@${offset}`);
}
log('All chunks sent ✓');
await sleep(150);
log('\n── Start program ──');
await writeCmd(new Uint8Array([CMD_START_USER_PROGRAM, 0]), 'START');
setStatus('Running');
log('Program started ✓ — stdout will show as [stdout] lines');
setStatus('Upload complete');
log('Upload complete. Press the hub button to run.');
} catch (e) {
setStatus('Upload failed');
log('Error: ' + e.message);
log(`\nUpload error: ${e.name} ${e.message}`);
console.error(e);
} finally {
document.getElementById('uploadBtn').disabled = false;
}
}
function onNotify(event) {
const data = new Uint8Array(event.target.value.buffer);
if (data[0] === 0x01) {
log('[stdout] ' + new TextDecoder().decode(data.slice(1)));
} else if (data[0] === 0x00) {
log('[status] Hub status code: ' + data[1]); // 0x00 = idle, etc.
} else {
log('[event 0x' + data[0].toString(16) + '] ' + Array.from(data).join(','));
async function stopProgram() {
if (!cmdChar) return;
try {
await writeCmd(new Uint8Array([CMD_STOP_USER_PROGRAM]), 'STOP');
log('Stop sent ✓');
} catch (e) {
log(`Stop error: ${e.name} ${e.message}`);
}
}
async function disconnect() {
if (device && device.gatt.connected) {
device.gatt.disconnect();

BIN
main.mpy

Binary file not shown.

View File

@@ -1,6 +1,10 @@
from pybricks.hubs import PrimeHub
from pybricks.parameters import Color
from pybricks.tools import wait
import micropython
hub = PrimeHub()
hub.light.on(Color.GREEN)
wait(1000)
hub.light.on(Color.RED)
wait(100)
print("Hello, World!")
print("This is the spike prime hub.")
print(micropython.mem_info())

BIN
main2.mpy

Binary file not shown.