Compare commits
2 Commits
d4bd879577
...
e26b52bc7e
| Author | SHA1 | Date | |
|---|---|---|---|
| e26b52bc7e | |||
| 998ab7d716 |
47
compile.js
Normal file
47
compile.js
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
"use strict";
|
||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
// Patch fetch before loading the module so the WASM loader never reaches it
|
||||||
|
const wasmPath = path.join(
|
||||||
|
path.dirname(require.resolve("@pybricks/mpy-cross-v6/build/mpy-cross-v6.js")),
|
||||||
|
"mpy-cross-v6.wasm",
|
||||||
|
);
|
||||||
|
const wasmBinary = fs.readFileSync(wasmPath);
|
||||||
|
|
||||||
|
// Monkey-patch the Emscripten module to inject wasmBinary
|
||||||
|
const originalMpyCross = require("@pybricks/mpy-cross-v6/build/mpy-cross-v6");
|
||||||
|
const MpyCross = (opts) => originalMpyCross({ ...opts, wasmBinary });
|
||||||
|
|
||||||
|
function compile(fileName, fileContents, options) {
|
||||||
|
return new Promise(function (resolve, reject) {
|
||||||
|
try {
|
||||||
|
const args = [...(options || []), fileName];
|
||||||
|
MpyCross({
|
||||||
|
arguments: ["-o", "main.mpy", "__main__.py"],
|
||||||
|
inputFileContents: fileContents,
|
||||||
|
callback: function (status, mpy, out, err) {
|
||||||
|
resolve({ status, mpy, out, err });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const myCode = `
|
||||||
|
from pybricks.hubs import PrimeHub
|
||||||
|
from pybricks.parameters import Color
|
||||||
|
hub = PrimeHub()
|
||||||
|
hub.light.on(Color.GREEN)
|
||||||
|
`;
|
||||||
|
|
||||||
|
compile("main.py", myCode, []).then((result) => {
|
||||||
|
if (result.status === 0) {
|
||||||
|
fs.writeFileSync("main.mpy", result.mpy);
|
||||||
|
console.log("Success! Written to main.mpy");
|
||||||
|
} else {
|
||||||
|
console.error("Compiler error:", result.err);
|
||||||
|
}
|
||||||
|
});
|
||||||
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>
|
||||||
4
main.py
Normal file
4
main.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
from pybricks.hubs import PrimeHub
|
||||||
|
from pybricks.parameters import Color
|
||||||
|
hub = PrimeHub()
|
||||||
|
hub.light.on(Color.GREEN)
|
||||||
71
obsoletecompiler.html
Normal file
71
obsoletecompiler.html
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Pybricks Compiler</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<pre id="out">Compiling...</pre>
|
||||||
|
<script>
|
||||||
|
const WASM_URL = 'https://cdn.jsdelivr.net/npm/@pybricks/mpy-cross-v6@2.0.0/build/mpy-cross-v6.wasm';
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
const out = document.getElementById('out');
|
||||||
|
|
||||||
|
// Load mpy-cross JS from CDN
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
const s = document.createElement('script');
|
||||||
|
s.src = 'https://cdn.jsdelivr.net/npm/@pybricks/mpy-cross-v6@2.0.0/build/mpy-cross-v6.js';
|
||||||
|
s.onload = resolve;
|
||||||
|
s.onerror = reject;
|
||||||
|
document.head.appendChild(s);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fetch main.py from same directory as this HTML file
|
||||||
|
const code = await fetch('./main.py').then(r => r.text());
|
||||||
|
|
||||||
|
// Compile
|
||||||
|
const result = await new Promise((resolve, reject) => {
|
||||||
|
try {
|
||||||
|
MpyCross({
|
||||||
|
arguments: ['main.py'],
|
||||||
|
inputFileContents: code,
|
||||||
|
locateFile: (path) => path === 'mpy-cross-v6.wasm' ? WASM_URL : path,
|
||||||
|
callback: (status, mpy, out, err) => resolve({ status, mpy, out, err }),
|
||||||
|
});
|
||||||
|
} catch (e) { reject(e); }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.status !== 0) {
|
||||||
|
out.textContent = 'Error:\n' + result.err;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrap with __main__ header to match pybricksdev multi-file format
|
||||||
|
const name = '__main__';
|
||||||
|
const nameBytes = new TextEncoder().encode(name + '\x00');
|
||||||
|
const sizeBytes = new Uint8Array(4);
|
||||||
|
new DataView(sizeBytes.buffer).setUint32(0, result.mpy.length, true);
|
||||||
|
const wrapped = new Uint8Array(sizeBytes.length + nameBytes.length + result.mpy.length);
|
||||||
|
wrapped.set(sizeBytes, 0);
|
||||||
|
wrapped.set(nameBytes, sizeBytes.length);
|
||||||
|
wrapped.set(result.mpy, sizeBytes.length + nameBytes.length);
|
||||||
|
|
||||||
|
// Auto-download the .mpy
|
||||||
|
const blob = new Blob([wrapped], { type: 'application/octet-stream' });
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = URL.createObjectURL(blob);
|
||||||
|
a.download = 'main.mpy';
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(a.href);
|
||||||
|
|
||||||
|
out.textContent = 'Done! ' + wrapped.length + ' bytes — check your downloads.';
|
||||||
|
}
|
||||||
|
|
||||||
|
run().catch(e => document.getElementById('out').textContent = 'Error: ' + (e?.stack || e?.message || JSON.stringify(e)));
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
22
package-lock.json
generated
Normal file
22
package-lock.json
generated
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"name": "pynamics-web",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "pynamics-web",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"@pybricks/mpy-cross-v6": "^2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@pybricks/mpy-cross-v6": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@pybricks/mpy-cross-v6/-/mpy-cross-v6-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-DJqQgpAyoUR/myQCJFl27WxXfQavVx3nYLMOpEAuxXRcYyDebHLx4fsUOhLV+vK1LfEqeBpzAjMMSh2U4Q+dtA==",
|
||||||
|
"license": "MIT"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
14
package.json
Normal file
14
package.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"name": "pynamics-web",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Web app for Pynamics",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"@pybricks/mpy-cross-v6": "^2.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
tests/main.mpy
Normal file
BIN
tests/main.mpy
Normal file
Binary file not shown.
BIN
tests/main2.mpy
Normal file
BIN
tests/main2.mpy
Normal file
Binary file not shown.
Reference in New Issue
Block a user