Added multi-file support
This commit is contained in:
333
main.html
333
main.html
@@ -45,82 +45,131 @@
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
#stdinBar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 12px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
#stdinLabel {
|
||||
font-size: 12px;
|
||||
color: #e8ff47;
|
||||
white-space: nowrap;
|
||||
user-select: none;
|
||||
min-width: 52px;
|
||||
}
|
||||
|
||||
#stdinInput {
|
||||
flex: 1;
|
||||
background: #111118;
|
||||
color: #e0e0f0;
|
||||
border: 1px solid #333;
|
||||
padding: 6px 10px;
|
||||
font-family: monospace;
|
||||
font-size: 13px;
|
||||
outline: none;
|
||||
transition: border-color 0.15s;
|
||||
}
|
||||
|
||||
#stdinInput:focus {
|
||||
border-color: #e8ff47;
|
||||
}
|
||||
|
||||
#stdinInput:disabled {
|
||||
opacity: 0.35;
|
||||
}
|
||||
|
||||
#sendBtn {
|
||||
background: #e8ff47;
|
||||
color: #0a0a0f;
|
||||
border: none;
|
||||
padding: 6px 14px;
|
||||
font-family: monospace;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
#sendBtn:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<button id="connectBtn" onclick="connect()">Connect</button>
|
||||
<button id="uploadBtn" onclick="upload()" disabled>Upload & Run</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="stdinBar">
|
||||
<span id="stdinLabel">stdin></span>
|
||||
<input id="stdinInput" type="text"
|
||||
placeholder="Type input and press Enter or Send… (only send when program is waiting)"
|
||||
onkeydown="onStdinKey(event)" autocomplete="off" spellcheck="false" disabled />
|
||||
<button id="sendBtn" onclick="sendStdin()" disabled>Send</button>
|
||||
</div>
|
||||
|
||||
<div id="log"></div>
|
||||
|
||||
<script>
|
||||
// UUIDs for Pybricks service / characteristics (fixed by firmware)
|
||||
// ── Pybricks BLE UUIDs (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';
|
||||
|
||||
// Device Information Service UUIDs
|
||||
// Device Information Service
|
||||
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';
|
||||
|
||||
// Commands (PBIO_PYBRICKS_COMMAND_*)
|
||||
const CMD_STOP_USER_PROGRAM = 0;
|
||||
const CMD_START_USER_PROGRAM = 1;
|
||||
const CMD_WRITE_USER_PROGRAM_META = 3;
|
||||
const CMD_WRITE_USER_RAM = 4;
|
||||
// Command bytes (PBIO_PYBRICKS_COMMAND_*)
|
||||
const CMD_STOP_USER_PROGRAM = 0; // v1.0.0
|
||||
const CMD_START_USER_PROGRAM = 1; // v1.2.0
|
||||
const CMD_WRITE_USER_RAM = 4; // v1.2.0
|
||||
const CMD_WRITE_STDIN = 6; // v1.3.0
|
||||
|
||||
// Events (PBIO_PYBRICKS_EVENT_*)
|
||||
// Event bytes (PBIO_PYBRICKS_EVENT_*)
|
||||
const EVT_STATUS_REPORT = 0;
|
||||
const EVT_WRITE_STDOUT = 1;
|
||||
|
||||
// ── State ─────────────────────────────────────────────────────────────────
|
||||
let device, server, cmdChar;
|
||||
let maxCharSize = 20;
|
||||
let lastStatusFlags = 0;
|
||||
|
||||
// stdout reassembly — BLE can fragment across multiple notifications
|
||||
let stdoutBuffer = '';
|
||||
let flushTimer = null;
|
||||
|
||||
// ── Pynamics ──────────────────────────────────────────────────────────────
|
||||
// Matches: ESC[?PYN;<EVENT_ID>;<arg1>;<arg2>;...~
|
||||
const PYNAMICS_REGEX = /\x1b\[\?PYN;([^;~]+);([^~]*?)~/g;
|
||||
|
||||
const PYNAMICS_EVENT_MAP = {
|
||||
'1': 'click',
|
||||
'2': 'focus',
|
||||
'3': 'blur',
|
||||
'4': 'navigate',
|
||||
'5': 'state_change',
|
||||
'6': 'notify',
|
||||
'7': 'render',
|
||||
'99': 'custom',
|
||||
// ── Pynamics escape-code parser ───────────────────────────────────────────
|
||||
// Matches: ESC [ ? PYN ; <eventId> ; <arg1> ; <arg2> ; ... ~
|
||||
const PYNAMICS_RE = /\x1b\[\?PYN;([^;~]+);([^~]*?)~/g;
|
||||
const PYNAMICS_EVENTS = {
|
||||
'1': 'click', '2': 'focus', '3': 'blur', '4': 'navigate',
|
||||
'5': 'state_change', '6': 'notify', '7': 'render', '99': 'custom',
|
||||
};
|
||||
|
||||
/**
|
||||
* Strips all Pynamics escape codes from a string, fires console.log for
|
||||
* each one, and returns the cleaned string (without the escape sequences).
|
||||
*/
|
||||
function processPynamics(text) {
|
||||
PYNAMICS_RE.lastIndex = 0;
|
||||
let summary = '';
|
||||
let match;
|
||||
// Reset lastIndex so repeated calls work correctly
|
||||
PYNAMICS_REGEX.lastIndex = 0;
|
||||
|
||||
while ((match = PYNAMICS_REGEX.exec(text)) !== null) {
|
||||
const eventId = match[1];
|
||||
const args = match[2].split(';').filter(Boolean);
|
||||
const name = PYNAMICS_EVENT_MAP[eventId] ?? `unknown(${eventId})`;
|
||||
|
||||
console.log('[pynamics]', { event: name, eventId, args });
|
||||
while ((match = PYNAMICS_RE.exec(text)) !== null) {
|
||||
const name = PYNAMICS_EVENTS[match[1]] ?? `unknown(${match[1]})`;
|
||||
const args = match[2].split(';').filter(Boolean);
|
||||
console.log('[pynamics]', { event: name, args });
|
||||
summary += `[PYN:${name}(${args.join(',')})] `;
|
||||
}
|
||||
|
||||
// Remove all escape sequences from the visible text
|
||||
return text.replace(PYNAMICS_REGEX, '');
|
||||
return summary + text.replace(PYNAMICS_RE, '').trim();
|
||||
}
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
// ── Logging helpers ───────────────────────────────────────────────────────
|
||||
function log(msg) {
|
||||
const el = document.getElementById('log');
|
||||
el.textContent += msg + '\n';
|
||||
@@ -129,18 +178,16 @@
|
||||
function setStatus(msg) {
|
||||
document.getElementById('status').textContent = msg;
|
||||
}
|
||||
function sleep(ms) {
|
||||
return new Promise(r => setTimeout(r, ms));
|
||||
}
|
||||
function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
|
||||
|
||||
// ── Binary helpers ────────────────────────────────────────────────────────
|
||||
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);
|
||||
const out = new Uint8Array(arrays.reduce((s, a) => s + a.length, 0));
|
||||
let off = 0;
|
||||
for (const a of arrays) { out.set(a, off); off += a.length; }
|
||||
return out;
|
||||
@@ -148,18 +195,19 @@
|
||||
function hexpreview(data, n = 12) {
|
||||
return Array.from(data.slice(0, n))
|
||||
.map(b => b.toString(16).padStart(2, '0')).join(' ') +
|
||||
(data.length > n ? ' ...' : '');
|
||||
(data.length > n ? ' …' : '');
|
||||
}
|
||||
|
||||
// ── BLE write with one retry ──────────────────────────────────────────────
|
||||
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}`);
|
||||
log(` TX error (${label}): ${e.name} — ${e.message}`);
|
||||
if (e.name === 'NetworkError' || e.name === 'NotSupportedError') {
|
||||
await sleep(200);
|
||||
log(' Retrying once...');
|
||||
log(' Retrying…');
|
||||
await cmdChar.writeValueWithResponse(data);
|
||||
} else {
|
||||
throw e;
|
||||
@@ -167,53 +215,116 @@
|
||||
}
|
||||
}
|
||||
|
||||
// ── Status flags ──────────────────────────────────────────────────────────
|
||||
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}`;
|
||||
return [
|
||||
`running=${!!(flags & (1 << 6))}`,
|
||||
`btn=${!!(flags & (1 << 5))}`,
|
||||
`hostConn=${!!(flags & (1 << 9))}`,
|
||||
`fileIO=${!!(flags & (1 << 13))}`,
|
||||
`shutdown=${!!(flags & (1 << 7))}`,
|
||||
].join(' ');
|
||||
}
|
||||
function isRunning() { return !!(lastStatusFlags & (1 << 6)); }
|
||||
|
||||
function isBusy() {
|
||||
return !!(lastStatusFlags & (1 << 6)) || !!(lastStatusFlags & (1 << 13));
|
||||
// ── stdout handling ───────────────────────────────────────────────────────
|
||||
// MicroPython writes stdout in BLE-sized chunks with no guarantee that a
|
||||
// newline lands at a packet boundary. input() prompts never end in \n.
|
||||
//
|
||||
// Strategy:
|
||||
// • Complete lines (\n-terminated) are flushed immediately.
|
||||
// • Any trailing fragment is held for 80 ms. If another packet arrives
|
||||
// within that window the timer resets (handles mid-word BLE splits).
|
||||
// After 80 ms of silence the fragment is printed (handles input() prompts).
|
||||
// • On disconnect, whatever remains is flushed immediately.
|
||||
|
||||
function flushPartial() {
|
||||
clearTimeout(flushTimer);
|
||||
flushTimer = null;
|
||||
if (!stdoutBuffer) return;
|
||||
const clean = processPynamics(stdoutBuffer);
|
||||
if (clean.trim()) log('[stdout] ' + clean);
|
||||
stdoutBuffer = '';
|
||||
}
|
||||
|
||||
function onNotify(event) {
|
||||
const data = new Uint8Array(event.target.value.buffer);
|
||||
if (data.length === 0) { log('[event] empty notification'); return; }
|
||||
if (!data.length) { log('[event] empty notification'); return; }
|
||||
|
||||
const type = data[0];
|
||||
|
||||
if (type === EVT_WRITE_STDOUT) {
|
||||
// Cancel pending partial flush — more data just arrived
|
||||
clearTimeout(flushTimer);
|
||||
flushTimer = null;
|
||||
|
||||
stdoutBuffer += new TextDecoder().decode(data.slice(1));
|
||||
|
||||
// Flush every complete \n-terminated line immediately
|
||||
const lines = stdoutBuffer.split('\n');
|
||||
stdoutBuffer = lines.pop() || '';
|
||||
stdoutBuffer = lines.pop(); // trailing fragment (no \n yet)
|
||||
|
||||
for (const line of lines) {
|
||||
// Strip Pynamics codes and fire console events before displaying
|
||||
const clean = processPynamics(line);
|
||||
if (clean.trim()) {
|
||||
log('[stdout] ' + clean);
|
||||
}
|
||||
if (clean.trim()) log('[stdout] ' + clean);
|
||||
}
|
||||
|
||||
// Schedule flush of any remaining fragment after 80 ms of silence
|
||||
if (stdoutBuffer) {
|
||||
flushTimer = setTimeout(flushPartial, 80);
|
||||
}
|
||||
|
||||
} else if (type === EVT_STATUS_REPORT) {
|
||||
const view = new DataView(data.buffer);
|
||||
const flags = view.getUint32(1, true);
|
||||
const flags = new DataView(data.buffer).getUint32(1, true);
|
||||
log(`[status] 0x${flags.toString(16).padStart(8, '0')} | ${decodeStatus(flags)}`);
|
||||
} else {
|
||||
log(`[event 0x${type.toString(16)}] ${hexpreview(data)}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ── stdin ─────────────────────────────────────────────────────────────────
|
||||
// The Pybricks protocol (v1.3.0+) defines CMD_WRITE_STDIN = 0x06.
|
||||
// Payload is raw bytes; no framing. MicroPython's input() blocks until
|
||||
// it receives a \n, so we always append one.
|
||||
//
|
||||
// IMPORTANT: stdin bytes are queued on the hub regardless of whether
|
||||
// input() is currently blocking. Sending before the program reaches
|
||||
// input() means the \n will be consumed immediately and the prompt will
|
||||
// appear to be skipped. Always wait until you see the prompt in stdout.
|
||||
|
||||
function setConnected(yes) {
|
||||
document.getElementById('connectBtn').disabled = yes;
|
||||
document.getElementById('uploadBtn').disabled = !yes;
|
||||
document.getElementById('stopBtn').disabled = !yes;
|
||||
document.getElementById('disconnectBtn').disabled = !yes;
|
||||
document.getElementById('stdinInput').disabled = !yes;
|
||||
document.getElementById('sendBtn').disabled = !yes;
|
||||
}
|
||||
|
||||
function onStdinKey(e) {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault(); // Prevent the browser from doing anything else
|
||||
sendStdin();
|
||||
}
|
||||
}
|
||||
|
||||
async function sendStdin() {
|
||||
const inputEl = document.getElementById('stdinInput');
|
||||
const text = inputEl.value;
|
||||
if (text === "" && !confirm("Send empty newline?")) return; // Optional safety check
|
||||
|
||||
inputEl.value = ""; // Clear it NOW
|
||||
|
||||
const encoded = new TextEncoder().encode(text + '\n');
|
||||
const packet = concat(new Uint8Array([CMD_WRITE_STDIN]), encoded);
|
||||
await writeCmd(packet, 'STDIN');
|
||||
}
|
||||
|
||||
// ── Connect ───────────────────────────────────────────────────────────────
|
||||
async function connect() {
|
||||
try {
|
||||
setStatus('Scanning...');
|
||||
setStatus('Scanning…');
|
||||
device = await navigator.bluetooth.requestDevice({
|
||||
filters: [{ services: [PYBRICKS_SERVICE_UUID] }],
|
||||
optionalServices: [
|
||||
@@ -221,87 +332,78 @@
|
||||
PYBRICKS_CMD_EVENT_UUID,
|
||||
PYBRICKS_CAPABILITIES_UUID,
|
||||
DI_SERVICE_UUID,
|
||||
]
|
||||
],
|
||||
});
|
||||
|
||||
device.addEventListener('gattserverdisconnected', onDisconnect);
|
||||
|
||||
setStatus('Connecting GATT...');
|
||||
setStatus('Connecting GATT…');
|
||||
server = await device.gatt.connect();
|
||||
|
||||
// Device Information Service
|
||||
try {
|
||||
const di = await server.getPrimaryService(DI_SERVICE_UUID);
|
||||
const sw = new TextDecoder().decode(
|
||||
await (await di.getCharacteristic(SW_REV_UUID)).readValue()
|
||||
);
|
||||
await (await di.getCharacteristic(SW_REV_UUID)).readValue());
|
||||
const fw = new TextDecoder().decode(
|
||||
await (await di.getCharacteristic(FW_REV_UUID)).readValue()
|
||||
);
|
||||
await (await di.getCharacteristic(FW_REV_UUID)).readValue());
|
||||
log(`Protocol version : ${sw}`);
|
||||
log(`Firmware version : ${fw}`);
|
||||
} catch (e) {
|
||||
log(`DI service: ${e.message}`);
|
||||
}
|
||||
} catch (e) { log(`DI service: ${e.message}`); }
|
||||
|
||||
// Hub capabilities
|
||||
const svc = await server.getPrimaryService(PYBRICKS_SERVICE_UUID);
|
||||
|
||||
try {
|
||||
const capChar = await svc.getCharacteristic(PYBRICKS_CAPABILITIES_UUID);
|
||||
const raw = await capChar.readValue();
|
||||
const raw = await (await svc.getCharacteristic(PYBRICKS_CAPABILITIES_UUID)).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 maxProgBytes = 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(`Max program size : ${maxProgBytes} bytes`);
|
||||
log(`Slots : ${numSlots}`);
|
||||
log(`MPY v6: ${!!(features & 2)} | v6.1: ${!!(features & 4)} | v6.3: ${!!(features & 32)}`);
|
||||
} catch (e) {
|
||||
log(`Capabilities: ${e.message}`);
|
||||
}
|
||||
} catch (e) { log(`Capabilities: ${e.message}`); }
|
||||
|
||||
// Command/event characteristic
|
||||
cmdChar = await svc.getCharacteristic(PYBRICKS_CMD_EVENT_UUID);
|
||||
log(`Char props: write=${cmdChar.properties.write} writeWOR=${cmdChar.properties.writeWithoutResponse} notify=${cmdChar.properties.notify}`);
|
||||
log(`Char props: write=${cmdChar.properties.write} ` +
|
||||
`writeWOR=${cmdChar.properties.writeWithoutResponse} ` +
|
||||
`notify=${cmdChar.properties.notify}`);
|
||||
|
||||
await cmdChar.startNotifications();
|
||||
cmdChar.addEventListener('characteristicvaluechanged', onNotify);
|
||||
|
||||
await sleep(500);
|
||||
|
||||
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;
|
||||
setConnected(true);
|
||||
|
||||
} catch (e) {
|
||||
setStatus('Connection failed');
|
||||
log(`Error: ${e.name} ${e.message}`);
|
||||
log(`Error: ${e.name} — ${e.message}`);
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
function onDisconnect() {
|
||||
flushPartial(); // emit anything still buffered
|
||||
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;
|
||||
setConnected(false);
|
||||
}
|
||||
|
||||
// ── Upload & run ──────────────────────────────────────────────────────────
|
||||
async function upload() {
|
||||
if (!cmdChar) { log('Not connected to hub.'); return; }
|
||||
if (!cmdChar) { log('Not connected.'); return; }
|
||||
|
||||
log('Stopping any running program...');
|
||||
log('Stopping any running program…');
|
||||
try {
|
||||
await writeCmd(new Uint8Array([CMD_STOP_USER_PROGRAM]), 'STOP');
|
||||
await sleep(200);
|
||||
} catch (e) {
|
||||
log('Stop command ignored (no program running)');
|
||||
log('Stop ignored (nothing running)');
|
||||
}
|
||||
|
||||
document.getElementById('uploadBtn').disabled = true;
|
||||
@@ -309,17 +411,15 @@
|
||||
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)}`);
|
||||
log(`\nLoaded main.bin : ${blob.length} bytes`);
|
||||
log(`Preview : ${hexpreview(blob, 20)}`);
|
||||
|
||||
const maxProgSize = 261512;
|
||||
if (blob.length > maxProgSize) throw new Error(`Program too large (max ${maxProgSize} bytes)`);
|
||||
if (blob.length > 261512) throw new Error('Program too large (max 261512 bytes)');
|
||||
|
||||
setStatus('Uploading...');
|
||||
|
||||
const PAYLOAD = maxCharSize - 5;
|
||||
const chunks = Math.ceil(blob.length / PAYLOAD);
|
||||
log(`\n── Upload ${blob.length} bytes in ${chunks} chunks ──`);
|
||||
setStatus('Uploading…');
|
||||
const PAYLOAD = maxCharSize - 5; // 1 cmd + 4 offset
|
||||
const nChunks = Math.ceil(blob.length / PAYLOAD);
|
||||
log(`\n── Upload ${blob.length} bytes in ${nChunks} chunk(s) ──`);
|
||||
|
||||
for (let offset = 0; offset < blob.length; offset += PAYLOAD) {
|
||||
const chunk = blob.slice(offset, Math.min(offset + PAYLOAD, blob.length));
|
||||
@@ -327,36 +427,39 @@
|
||||
await writeCmd(packet, `RAM@${offset}`);
|
||||
}
|
||||
log('All chunks sent ✓');
|
||||
|
||||
await sleep(150);
|
||||
|
||||
// Inside your upload() function...
|
||||
log('\n── Start program ──');
|
||||
await writeCmd(new Uint8Array([CMD_START_USER_PROGRAM, 0]), 'START');
|
||||
stdoutBuffer = ''; // Clear the JS side buffer
|
||||
if (flushTimer) clearTimeout(flushTimer);
|
||||
|
||||
await writeCmd(new Uint8Array([CMD_START_USER_PROGRAM, 0]), 'START');
|
||||
setStatus('Running');
|
||||
log('Program started ✓ — stdout will show as [stdout] lines');
|
||||
log('Program started ✓\n');
|
||||
|
||||
} catch (e) {
|
||||
setStatus('Upload failed');
|
||||
log(`\nUpload error: ${e.name} ${e.message}`);
|
||||
log(`\nUpload error: ${e.name} — ${e.message}`);
|
||||
console.error(e);
|
||||
} finally {
|
||||
document.getElementById('uploadBtn').disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Stop / disconnect ─────────────────────────────────────────────────────
|
||||
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}`);
|
||||
log(`Stop error: ${e.name} — ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function disconnect() {
|
||||
if (device && device.gatt.connected) device.gatt.disconnect();
|
||||
if (device?.gatt.connected) device.gatt.disconnect();
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user